xgpsspeed.in 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. #!@PYSHEBANG@
  2. # @GENERATED@
  3. #
  4. # by
  5. # Robin Wittler <real@the-real.org> (speedometer mode)
  6. # and
  7. # Chen Wei <weichen302@gmx.com> (nautical mode)
  8. #
  9. # This file is Copyright 2010 by the GPSD project
  10. # SPDX-License-Identifier: BSD-2-clause
  11. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  12. # Preserve this property!
  13. from __future__ import absolute_import, print_function, division
  14. import argparse
  15. import cairo
  16. from math import pi
  17. from math import cos
  18. from math import sin
  19. from math import sqrt
  20. from math import radians
  21. import os
  22. from socket import error as SocketError
  23. import sys
  24. # Gtk3 imports. Gtk3 requires the require_version(), which then causes
  25. # pylint to complain about the subsequent "non-top" imports.
  26. try:
  27. import gi
  28. gi.require_version('Gtk', '3.0')
  29. except ImportError as err:
  30. # ModuleNotFoundError needs Python 3.6
  31. sys.stderr.write("xgps: ERROR %s\n" % err)
  32. sys.exit(1)
  33. except ValueError as err:
  34. # Gtk2 may be installed, has no require_version()
  35. sys.stderr.write("xgps: ERROR %s\n" % err)
  36. sys.exit(1)
  37. from gi.repository import Gtk # pylint: disable=wrong-import-position
  38. from gi.repository import Gdk # pylint: disable=wrong-import-position
  39. from gi.repository import GdkPixbuf # pylint: disable=wrong-import-position
  40. from gi.repository import GLib # pylint: disable=wrong-import-position
  41. # pylint wants local modules last
  42. try:
  43. import gps
  44. except ImportError as e:
  45. sys.stderr.write(
  46. "xgpsspeed: can't load Python gps libraries -- check PYTHONPATH.\n")
  47. sys.stderr.write("%s\n" % e)
  48. sys.exit(1)
  49. gps_version = '@VERSION@'
  50. if gps.__version__ != gps_version:
  51. sys.stderr.write("xgpsspeed: ERROR: need gps module version %s, got %s\n" %
  52. (gps_version, gps.__version__))
  53. sys.exit(1)
  54. class Speedometer(Gtk.DrawingArea):
  55. "Speedometer class"
  56. def __init__(self, speed_unit=None):
  57. "Init Speedometer class"
  58. Gtk.DrawingArea.__init__(self)
  59. self.MPH_UNIT_LABEL = 'mph'
  60. self.KPH_UNIT_LABEL = 'kmh'
  61. self.KNOTS_UNIT_LABEL = 'knots'
  62. self.conversions = {
  63. self.MPH_UNIT_LABEL: gps.MPS_TO_MPH,
  64. self.KPH_UNIT_LABEL: gps.MPS_TO_KPH,
  65. self.KNOTS_UNIT_LABEL: gps.MPS_TO_KNOTS
  66. }
  67. self.speed_unit = speed_unit or self.MPH_UNIT_LABEL
  68. if self.speed_unit not in self.conversions:
  69. raise TypeError(
  70. '%s is not a valid speed unit'
  71. % (repr(speed_unit))
  72. )
  73. class LandSpeedometer(Speedometer):
  74. "LandSpeedometer class"
  75. def __init__(self, speed_unit=None):
  76. "Init LandSpeedometer class"
  77. Speedometer.__init__(self, speed_unit)
  78. self.connect('size-allocate', self.on_size_allocate)
  79. self.width = self.height = 0
  80. self.connect('draw', self.draw_s)
  81. self.long_ticks = (2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8)
  82. self.short_ticks = (0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9)
  83. self.long_inset = lambda x: 0.1 * x
  84. self.middle_inset = lambda x: self.long_inset(x) / 1.5
  85. self.short_inset = lambda x: self.long_inset(x) / 3
  86. self.res_div = 10.0
  87. self.res_div_mul = 1
  88. self.last_speed = 0
  89. self.nums = {
  90. -8: 0,
  91. -7: 10,
  92. -6: 20,
  93. -5: 30,
  94. -4: 40,
  95. -3: 50,
  96. -2: 60,
  97. -1: 70,
  98. 0: 80,
  99. 1: 90,
  100. 2: 100
  101. }
  102. def on_size_allocate(self, _unused, allocation):
  103. self.width = allocation.width
  104. self.height = allocation.height
  105. def draw_s(self, widget, _event, _empty=None):
  106. "Top level draw"
  107. window = widget.get_window()
  108. region = window.get_clip_region()
  109. context = window.begin_draw_frame(region)
  110. self.cr = context.get_cairo_context()
  111. self.cr.rectangle(0, 0, self.width, self.height)
  112. self.cr.clip()
  113. x, y = self.get_x_y()
  114. width, height = self.get_window().get_geometry()[2:4]
  115. radius = self.get_radius(width, height)
  116. self.cr.set_line_width(radius / 100)
  117. self.draw_arc_and_ticks(width, height, radius, x, y)
  118. self.draw_needle(self.last_speed, radius, x, y)
  119. self.draw_speed_text(self.last_speed, radius, x, y)
  120. self.cr = None
  121. window.end_draw_frame(context)
  122. def draw_arc_and_ticks(self, width, height, radius, x, y):
  123. self.cr.set_source_rgb(1.0, 1.0, 1.0)
  124. self.cr.rectangle(0, 0, width, height)
  125. self.cr.fill()
  126. self.cr.set_source_rgb(0.0, 0.0, 0.0)
  127. # draw the speedometer arc
  128. self.cr.arc_negative(x, y, radius, radians(60), radians(120))
  129. self.cr.stroke()
  130. long_inset = self.long_inset(radius)
  131. middle_inset = self.middle_inset(radius)
  132. short_inset = self.short_inset(radius)
  133. # draw the ticks
  134. for i in self.long_ticks:
  135. self.cr.move_to(
  136. x + (radius - long_inset) * cos(i * pi / 6.0),
  137. y + (radius - long_inset) * sin(i * pi / 6.0)
  138. )
  139. self.cr.line_to(
  140. (x + (radius + (self.cr.get_line_width() / 2)) *
  141. cos(i * pi / 6.0)),
  142. (y + (radius + (self.cr.get_line_width() / 2)) *
  143. sin(i * pi / 6.0))
  144. )
  145. self.cr.select_font_face(
  146. 'Georgia',
  147. cairo.FONT_SLANT_NORMAL,
  148. )
  149. self.cr.set_font_size(radius / 10)
  150. self.cr.save()
  151. _num = str(self.nums.get(i) * self.res_div_mul)
  152. (
  153. _x_bearing,
  154. _y_bearing,
  155. t_width,
  156. t_height,
  157. _x_advance,
  158. _y_advance
  159. ) = self.cr.text_extents(_num)
  160. if i in (-8, -7, -6, -5, -4):
  161. self.cr.move_to(
  162. (x + (radius - long_inset - (t_width / 2)) *
  163. cos(i * pi / 6.0)),
  164. (y + (radius - long_inset - (t_height * 2)) *
  165. sin(i * pi / 6.0))
  166. )
  167. elif i in (-2, -1, 0, 2, 1):
  168. self.cr.move_to(
  169. (x + (radius - long_inset - (t_width * 1.5)) *
  170. cos(i * pi / 6.0)),
  171. (y + (radius - long_inset - (t_height * 2)) *
  172. sin(i * pi / 6.0))
  173. )
  174. elif i in (-3,):
  175. self.cr.move_to(
  176. (x - t_width / 2),
  177. (y - radius + self.long_inset(radius) * 2 + t_height)
  178. )
  179. self.cr.show_text(_num)
  180. self.cr.restore()
  181. if i != self.long_ticks[0]:
  182. self.cr.move_to(
  183. x + (radius - middle_inset) * cos((i + 0.5) * pi / 6.0),
  184. y + (radius - middle_inset) * sin((i + 0.5) * pi / 6.0)
  185. )
  186. self.cr.line_to(
  187. x + (radius + (self.cr.get_line_width() / 2)) *
  188. cos((i + 0.5) * pi / 6.0),
  189. y + (radius + (self.cr.get_line_width() / 2)) *
  190. sin((i + 0.5) * pi / 6.0)
  191. )
  192. for z in self.short_ticks:
  193. w_half = self.cr.get_line_width() / 2
  194. if i < 0:
  195. self.cr.move_to(
  196. x + (radius - short_inset) * cos((i + z) * pi / 6.0),
  197. y + (radius - short_inset) * sin((i + z) * pi / 6.0)
  198. )
  199. self.cr.line_to(
  200. x + (radius + w_half) * cos((i + z) * pi / 6.0),
  201. y + (radius + w_half) * sin((i + z) * pi / 6.0)
  202. )
  203. else:
  204. self.cr.move_to(
  205. x + (radius - short_inset) * cos((i - z) * pi / 6.0),
  206. y + (radius - short_inset) * sin((i - z) * pi / 6.0)
  207. )
  208. self.cr.line_to(
  209. x + (radius + w_half) * cos((i - z) * pi / 6.0),
  210. y + (radius + w_half) * sin((i - z) * pi / 6.0)
  211. )
  212. self.cr.stroke()
  213. def draw_needle(self, speed, radius, x, y):
  214. self.cr.save()
  215. inset = self.long_inset(radius)
  216. speed = speed * self.conversions.get(self.speed_unit)
  217. speed = speed / (self.res_div * self.res_div_mul)
  218. actual = self.long_ticks[-1] + speed
  219. if actual > self.long_ticks[0]:
  220. self.res_div_mul += 1
  221. speed = speed / (self.res_div * self.res_div_mul)
  222. actual = self.long_ticks[-1] + speed
  223. self.cr.move_to(x, y)
  224. self.cr.line_to(
  225. x + (radius - (2 * inset)) * cos(actual * pi / 6.0),
  226. y + (radius - (2 * inset)) * sin(actual * pi / 6.0)
  227. )
  228. self.cr.stroke()
  229. self.cr.restore()
  230. def draw_speed_text(self, speed, radius, x, y):
  231. self.cr.save()
  232. speed = '%.2f %s' % (
  233. speed * self.conversions.get(self.speed_unit),
  234. self.speed_unit
  235. )
  236. self.cr.select_font_face(
  237. 'Georgia',
  238. cairo.FONT_SLANT_NORMAL,
  239. # cairo.FONT_WEIGHT_BOLD
  240. )
  241. self.cr.set_font_size(radius / 10)
  242. _x_bearing, _y_bearing, t_width, _t_height = \
  243. self.cr.text_extents(speed)[:4]
  244. self.cr.move_to((x - t_width / 2),
  245. (y + radius) - self.long_inset(radius))
  246. self.cr.show_text(speed)
  247. self.cr.restore()
  248. def get_x_y(self):
  249. rect = self.get_allocation()
  250. x = (rect.x + rect.width / 2.0)
  251. y = (rect.y + rect.height / 2.0) - 20
  252. return x, y
  253. def get_radius(self, width, height):
  254. return min(width / 2.0, height / 2.0) - 20
  255. class NauticalSpeedometer(Speedometer):
  256. "NauticalSpeedometer class"
  257. HEADING_SAT_GAP = 0.8
  258. SAT_SIZE = 10 # radius of the satellite circle in skyview
  259. def __init__(self, speed_unit=None, maxspeed=100, rotate=0.0):
  260. Speedometer.__init__(self, speed_unit)
  261. self.connect('size-allocate', self.on_size_allocate)
  262. self.width = self.height = 0
  263. self.connect('draw', self.draw_s)
  264. self.long_inset = lambda x: 0.05 * x
  265. self.mid_inset = lambda x: self.long_inset(x) / 1.5
  266. self.short_inset = lambda x: self.long_inset(x) / 3
  267. self.last_speed = 0
  268. self.satellites = []
  269. self.last_heading = 0
  270. self.maxspeed = int(maxspeed)
  271. self.rotate = radians(rotate)
  272. self.cr = None
  273. def polar2xy(self, radius, angle, polex, poley):
  274. '''convert Polar coordinate to Cartesian coordinate system
  275. the y axis in pygtk points downward
  276. Args:
  277. radius:
  278. angle: azimuth from from Polar coordinate system, in radian
  279. polex and poley are the Cartesian coordinate of the pole
  280. return a tuple contains (x, y)'''
  281. return (polex + cos(angle) * radius, poley - sin(angle) * radius)
  282. def polar2xyr(self, radius, angle, polex, poley):
  283. '''Version of polar2xy that includes rotation'''
  284. angle = (angle + self.rotate) % (pi * 2) # Note reversed sense
  285. return self.polar2xy(radius, angle, polex, poley)
  286. def on_size_allocate(self, _unused, allocation):
  287. self.width = allocation.width
  288. self.height = allocation.height
  289. def draw_s(self, widget, _event, _empty=None):
  290. "Top level draw"
  291. window = widget.get_window()
  292. region = window.get_clip_region()
  293. context = window.begin_draw_frame(region)
  294. self.cr = context.get_cairo_context()
  295. self.cr.rectangle(0, 0, self.width, self.height)
  296. self.cr.clip()
  297. x, y = self.get_x_y()
  298. width, height = self.get_window().get_geometry()[2:4]
  299. radius = self.get_radius(width, height)
  300. self.cr.set_line_width(radius / 100)
  301. self.draw_arc_and_ticks(width, height, radius, x, y)
  302. self.draw_heading(20, self.last_heading, radius, x, y)
  303. for sat in self.satellites:
  304. self.draw_sat(sat, radius * NauticalSpeedometer.HEADING_SAT_GAP,
  305. x, y)
  306. self.draw_speed(radius, x, y)
  307. self.cr = None
  308. window.end_draw_frame(context)
  309. def draw_text(self, x, y, text, fontsize=10):
  310. '''draw text at given location
  311. Args:
  312. x, y is the center of textbox'''
  313. txt = str(text)
  314. self.cr.new_sub_path()
  315. self.cr.set_source_rgba(0, 0, 0)
  316. self.cr.select_font_face('Sans',
  317. cairo.FONT_SLANT_NORMAL,
  318. cairo.FONT_WEIGHT_BOLD)
  319. self.cr.set_font_size(fontsize)
  320. (_x_bearing, _y_bearing,
  321. t_width, t_height) = self.cr.text_extents(txt)[:4]
  322. # set the center of textbox
  323. self.cr.move_to(x - t_width / 2, y + t_height / 2)
  324. self.cr.show_text(txt)
  325. def draw_arc_and_ticks(self, width, height, radius, x, y):
  326. '''Draw a serial of circle, with ticks in outmost circle'''
  327. self.cr.set_source_rgb(1.0, 1.0, 1.0)
  328. self.cr.rectangle(0, 0, width, height)
  329. self.cr.fill()
  330. self.cr.set_source_rgba(0, 0, 0)
  331. # draw the speedmeter arc
  332. rspeed = radius + 50
  333. self.cr.arc(x, y, rspeed, 2 * pi / 3, 7 * pi / 3)
  334. self.cr.set_source_rgba(0, 0, 0, 1.0)
  335. self.cr.stroke()
  336. s_long = self.long_inset(rspeed)
  337. s_middle = self.mid_inset(radius)
  338. s_short = self.short_inset(radius)
  339. for i in range(11):
  340. # draw the large ticks
  341. alpha = (8 - i) * pi / 6
  342. self.cr.move_to(*self.polar2xy(rspeed, alpha, x, y))
  343. self.cr.set_line_width(radius / 100)
  344. self.cr.line_to(*self.polar2xy(rspeed - s_long, alpha, x, y))
  345. self.cr.stroke()
  346. self.cr.set_line_width(radius / 200)
  347. xf, yf = self.polar2xy(rspeed + 10, alpha, x, y)
  348. stxt = (self.maxspeed // 10) * i
  349. self.draw_text(xf, yf, stxt, fontsize=radius / 15)
  350. for i in range(1, 11):
  351. # middle tick
  352. alpha = (8 - i) * pi / 6
  353. beta = (17 - 2 * i) * pi / 12
  354. self.cr.move_to(*self.polar2xy(rspeed, beta, x, y))
  355. self.cr.line_to(*self.polar2xy(rspeed - s_middle, beta, x, y))
  356. # short tick
  357. for n in range(10):
  358. gamma = alpha + n * pi / 60
  359. self.cr.move_to(*self.polar2xy(rspeed, gamma, x, y))
  360. self.cr.line_to(*self.polar2xy(rspeed - s_short, gamma, x, y))
  361. # draw the heading arc
  362. self.cr.new_sub_path()
  363. self.cr.arc(x, y, radius, 0, 2 * pi)
  364. self.cr.stroke()
  365. self.cr.arc(x, y, radius - 20, 0, 2 * pi)
  366. self.cr.set_source_rgba(0, 0, 0, 0.20)
  367. self.cr.fill()
  368. self.cr.set_source_rgba(0, 0, 0)
  369. # heading label 90/180/270
  370. for n in range(0, 4):
  371. label = str(n * 90)
  372. # self.cr.set_source_rgba(0, 1, 0)
  373. # radius * (1 + NauticalSpeedometer.HEADING_SAT_GAP),
  374. tbox_x, tbox_y = self.polar2xyr(
  375. radius * 0.88,
  376. (1 - n) * pi / 2,
  377. x, y)
  378. self.draw_text(tbox_x, tbox_y,
  379. label, fontsize=radius / 20)
  380. # draw the satellite arcs
  381. skyradius = radius * NauticalSpeedometer.HEADING_SAT_GAP
  382. self.cr.set_line_width(radius / 200)
  383. self.cr.set_source_rgba(0, 0, 0)
  384. self.cr.arc(x, y, skyradius, 0, 2 * pi)
  385. self.cr.set_source_rgba(1, 1, 1)
  386. self.cr.fill()
  387. self.cr.set_source_rgba(0, 0, 0)
  388. self.cr.arc(x, y, skyradius * 2 / 3, 0, 2 * pi)
  389. self.cr.move_to(x + skyradius / 3, y) # Avoid line connecting circles
  390. self.cr.arc(x, y, skyradius / 3, 0, 2 * pi)
  391. # draw the cross hair
  392. self.cr.move_to(*self.polar2xyr(skyradius, 1.5 * pi, x, y))
  393. self.cr.line_to(*self.polar2xyr(skyradius, 0.5 * pi, x, y))
  394. self.cr.move_to(*self.polar2xyr(skyradius, 0.0, x, y))
  395. self.cr.line_to(*self.polar2xyr(skyradius, pi, x, y))
  396. self.cr.set_line_width(radius / 200)
  397. self.cr.stroke()
  398. long_inset = self.long_inset(radius)
  399. mid_inset = self.mid_inset(radius)
  400. short_inset = self.short_inset(radius)
  401. # draw the large ticks
  402. for i in range(12):
  403. agllong = i * pi / 6
  404. self.cr.move_to(*self.polar2xy(radius - long_inset, agllong, x, y))
  405. self.cr.line_to(*self.polar2xy(radius, agllong, x, y))
  406. self.cr.set_line_width(radius / 100)
  407. self.cr.stroke()
  408. self.cr.set_line_width(radius / 200)
  409. # middle tick
  410. aglmid = (i + 0.5) * pi / 6
  411. self.cr.move_to(*self.polar2xy(radius - mid_inset, aglmid, x, y))
  412. self.cr.line_to(*self.polar2xy(radius, aglmid, x, y))
  413. # short tick
  414. for n in range(1, 10):
  415. aglshrt = agllong + n * pi / 60
  416. self.cr.move_to(*self.polar2xy(radius - short_inset,
  417. aglshrt, x, y))
  418. self.cr.line_to(*self.polar2xy(radius, aglshrt, x, y))
  419. self.cr.stroke()
  420. def draw_heading(self, trig_height, heading, radius, x, y):
  421. hypo = trig_height * 2 / sqrt(3)
  422. h = (pi / 2 - radians(heading) + self.rotate) % (pi * 2) # to xyz
  423. self.cr.set_line_width(2)
  424. self.cr.set_source_rgba(0, 0.3, 0.2, 0.8)
  425. # the triangle pointer
  426. x0 = x + radius * cos(h)
  427. y0 = y - radius * sin(h)
  428. x1 = x0 + hypo * cos(7 * pi / 6 + h)
  429. y1 = y0 - hypo * sin(7 * pi / 6 + h)
  430. x2 = x0 + hypo * cos(5 * pi / 6 + h)
  431. y2 = y0 - hypo * sin(5 * pi / 6 + h)
  432. self.cr.move_to(x0, y0)
  433. self.cr.line_to(x1, y1)
  434. self.cr.line_to(x2, y2)
  435. self.cr.line_to(x0, y0)
  436. self.cr.close_path()
  437. self.cr.fill()
  438. self.cr.stroke()
  439. # heading text
  440. (tbox_x, tbox_y) = self.polar2xy(radius * 1.1, h, x, y)
  441. self.draw_text(tbox_x, tbox_y, int(heading), fontsize=radius / 15)
  442. # the ship shape, based on test and try
  443. shiplen = radius * NauticalSpeedometer.HEADING_SAT_GAP / 4
  444. xh, yh = self.polar2xy(shiplen * 2.3, h, x, y)
  445. xa, ya = self.polar2xy(shiplen * 2.2, h + pi - 0.3, x, y)
  446. xb, yb = self.polar2xy(shiplen * 2.2, h + pi + 0.3, x, y)
  447. xc, yc = self.polar2xy(shiplen * 1.4, h - pi / 5, x, y)
  448. xd, yd = self.polar2xy(shiplen * 1.4, h + pi / 5, x, y)
  449. self.cr.set_source_rgba(0, 0.3, 0.2, 0.5)
  450. self.cr.move_to(xa, ya)
  451. self.cr.line_to(xb, yb)
  452. self.cr.line_to(xc, yc)
  453. self.cr.line_to(xh, yh)
  454. self.cr.line_to(xd, yd)
  455. self.cr.close_path()
  456. self.cr.fill()
  457. # self.cr.stroke()
  458. def set_color(self, spec):
  459. '''Set foreground color for drawing.'''
  460. color = Gdk.RGBA()
  461. color.parse(spec)
  462. Gdk.cairo_set_source_rgba(self.cr, color)
  463. def draw_sat(self, satsoup, radius, x, y):
  464. """Given a sat's elevation, azimuth, SNR, draw it on the skyview
  465. Arg:
  466. satsoup: a dictionary {'el': xx, 'az': xx, 'ss': xx}
  467. """
  468. el, az = satsoup['el'], satsoup['az']
  469. if el == 0 and az == 0:
  470. return # Skip satellites with unknown position
  471. h = pi / 2 - radians(az) # to xy
  472. self.cr.set_line_width(2)
  473. self.cr.set_source_rgb(0, 0, 0)
  474. x0, y0 = self.polar2xyr(radius * (90 - el) // 90, h, x, y)
  475. self.cr.new_sub_path()
  476. if gps.is_sbas(satsoup['PRN']):
  477. self.cr.rectangle(x0 - NauticalSpeedometer.SAT_SIZE,
  478. y0 - NauticalSpeedometer.SAT_SIZE,
  479. NauticalSpeedometer.SAT_SIZE * 2,
  480. NauticalSpeedometer.SAT_SIZE * 2)
  481. else:
  482. self.cr.arc(x0, y0, NauticalSpeedometer.SAT_SIZE, 0, pi * 2.0)
  483. if satsoup['ss'] < 10:
  484. self.set_color('Gray')
  485. elif satsoup['ss'] < 30:
  486. self.set_color('Red')
  487. elif satsoup['ss'] < 35:
  488. self.set_color('Yellow')
  489. elif satsoup['ss'] < 40:
  490. self.set_color('Green3')
  491. else:
  492. self.set_color('Green1')
  493. if satsoup['used']:
  494. self.cr.fill()
  495. else:
  496. self.cr.stroke()
  497. self.draw_text(x0, y0, satsoup['PRN'], fontsize=15)
  498. def draw_speed(self, radius, x, y):
  499. self.cr.new_sub_path()
  500. self.cr.set_line_width(20)
  501. self.cr.set_source_rgba(0, 0, 0, 0.5)
  502. speed = self.last_speed * self.conversions.get(self.speed_unit)
  503. # cariol arc angle start at polar 0, going clockwise
  504. alpha = 4 * pi / 3
  505. beta = 2 * pi - alpha
  506. theta = 5 * pi * speed / (self.maxspeed * 3)
  507. self.cr.arc(x, y, radius + 40, beta, beta + theta)
  508. self.cr.stroke()
  509. # self.cr.close_path()
  510. # self.cr.fill()
  511. label = '%.2f %s' % (speed, self.speed_unit)
  512. self.draw_text(x, y + radius + 40, label, fontsize=20)
  513. def get_x_y(self):
  514. rect = self.get_allocation()
  515. x = (rect.x + rect.width / 2.0)
  516. y = (rect.y + rect.height / 2.0) - 20
  517. return x, y
  518. def get_radius(self, width, height):
  519. return min(width / 2.0, height / 2.0) - 70
  520. class Main(object):
  521. "Main"
  522. def __init__(self, host='localhost', port=gps.GPSD_PORT, device=None,
  523. debug=0, speed_unit=None, maxspeed=0, nautical=False,
  524. rotate=0.0, target=""):
  525. self.host = host
  526. self.port = port
  527. self.device = device
  528. self.debug = debug
  529. self.speed_unit = speed_unit
  530. self.maxspeed = maxspeed
  531. self.nautical = nautical
  532. self.rotate = rotate
  533. self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
  534. if not self.window.get_display():
  535. raise Exception("Can't open display")
  536. if target:
  537. target = " " + target
  538. self.window.set_title('xgpsspeed' + target)
  539. self.window.connect("delete-event", self.delete_event)
  540. vbox = Gtk.VBox(homogeneous=False, spacing=0)
  541. self.window.add(vbox)
  542. # menubar
  543. menubar = Gtk.MenuBar()
  544. vbox.pack_start(menubar, False, False, 0)
  545. agr = Gtk.AccelGroup()
  546. self.window.add_accel_group(agr)
  547. # need the widget before the menu as the menu building
  548. # calls the widget
  549. self.window.set_size_request(400, 450)
  550. if self.nautical:
  551. self.widget = NauticalSpeedometer(
  552. speed_unit=self.speed_unit,
  553. maxspeed=self.maxspeed,
  554. rotate=self.rotate)
  555. else:
  556. self.widget = LandSpeedometer(speed_unit=self.speed_unit)
  557. self.speedframe = Gtk.Frame()
  558. self.speedframe.add(self.widget)
  559. vbox.add(self.speedframe)
  560. self.window.connect('delete-event', self.delete_event)
  561. self.window.connect('destroy', self.destroy)
  562. self.window.present()
  563. # File
  564. topmenu = Gtk.MenuItem(label="File")
  565. menubar.append(topmenu)
  566. submenu = Gtk.Menu()
  567. topmenu.set_submenu(submenu)
  568. menui = Gtk.MenuItem(label="Quit")
  569. key, mod = Gtk.accelerator_parse("<Control>Q")
  570. menui.add_accelerator("activate", agr, key, mod,
  571. Gtk.AccelFlags.VISIBLE)
  572. menui.connect("activate", Gtk.main_quit)
  573. submenu.append(menui)
  574. # View
  575. topmenu = Gtk.MenuItem(label="View")
  576. menubar.append(topmenu)
  577. submenu = Gtk.Menu()
  578. topmenu.set_submenu(submenu)
  579. views = [["Nautical", False, "0", "Nautical"],
  580. ["Land", False, "1", "Land"],
  581. ]
  582. if self.nautical:
  583. views[0][1] = True
  584. else:
  585. views[1][1] = True
  586. menui = None
  587. for name, active, acc, handle in views:
  588. menui = Gtk.RadioMenuItem(group=menui, label=name)
  589. menui.set_active(active)
  590. menui.connect("activate", self.view_toggle, handle)
  591. if acc:
  592. key, mod = Gtk.accelerator_parse(acc)
  593. menui.add_accelerator("activate", agr, key, mod,
  594. Gtk.AccelFlags.VISIBLE)
  595. submenu.append(menui)
  596. # Units
  597. topmenu = Gtk.MenuItem(label="Units")
  598. menubar.append(topmenu)
  599. submenu = Gtk.Menu()
  600. topmenu.set_submenu(submenu)
  601. units = [["Imperial", True, "i", 'mph'],
  602. ["Nautical", False, "n", 'knots'],
  603. ["Metric", False, "m", 'kmh'],
  604. ]
  605. menui = None
  606. for name, active, acc, handle in units:
  607. menui = Gtk.RadioMenuItem(group=menui, label=name)
  608. menui.set_active(active)
  609. menui.connect("activate", self.set_units, handle)
  610. if acc:
  611. key, mod = Gtk.accelerator_parse(acc)
  612. menui.add_accelerator("activate", agr, key, mod,
  613. Gtk.AccelFlags.VISIBLE)
  614. submenu.append(menui)
  615. # Help
  616. topmenu = Gtk.MenuItem(label="Help")
  617. menubar.append(topmenu)
  618. submenu = Gtk.Menu()
  619. topmenu.set_submenu(submenu)
  620. menui = Gtk.MenuItem(label="About")
  621. menui.connect("activate", self.about)
  622. submenu.append(menui)
  623. # vbox.pack_start(menubar, False, False, 0)
  624. # vbox.add(self.speedframe)
  625. self.window.show_all()
  626. def about(self, _unused):
  627. "Show about dialog"
  628. about = Gtk.AboutDialog()
  629. about.set_program_name("xgpsspeed")
  630. about.set_version("Versions:\n"
  631. "xgpspeed %s\n"
  632. "PyGObject Version %d.%d.%d" %
  633. (gps_version, gi.version_info[0],
  634. gi.version_info[1], gi.version_info[2]))
  635. about.set_copyright("Copyright 2004-2019 by The GPSD Project")
  636. about.set_website("https://www.gpsd.io")
  637. about.set_website_label("https://www.gpsd.io")
  638. about.set_license("BSD-2-clause")
  639. iconpath = gps.__iconpath__ + '/gpsd-logo.png'
  640. if os.access(iconpath, os.R_OK):
  641. pixbuf = GdkPixbuf.Pixbuf.new_from_file(iconpath)
  642. about.set_logo(pixbuf)
  643. about.run()
  644. about.destroy()
  645. def delete_event(self, _widget, _event, _data=None):
  646. "Say goodbye nicely"
  647. Gtk.main_quit()
  648. return False
  649. def set_units(self, _unused, handle):
  650. "Change the display units."
  651. # print("set_units:", handle, self)
  652. self.widget.speed_unit = handle
  653. def watch(self, daemon, device):
  654. self.daemon = daemon
  655. self.device = device
  656. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  657. GLib.IO_IN, self.handle_response)
  658. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  659. GLib.IO_ERR, self.handle_hangup)
  660. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  661. GLib.IO_HUP, self.handle_hangup)
  662. return True
  663. def view_toggle(self, action, name):
  664. "Toggle widget view"
  665. if not action.get_active() or not name:
  666. # nothing to do
  667. return
  668. parent = self.widget.get_parent()
  669. if 'Nautical' == name:
  670. self.nautical = True
  671. widget = NauticalSpeedometer(
  672. speed_unit=self.speed_unit,
  673. maxspeed=self.maxspeed,
  674. rotate=self.rotate)
  675. else:
  676. self.nautical = False
  677. widget = LandSpeedometer(speed_unit=self.speed_unit)
  678. parent.remove(self.widget)
  679. parent.add(widget)
  680. self.widget = widget
  681. self.widget.show()
  682. def handle_response(self, source, condition):
  683. if self.daemon.read() == -1:
  684. self.handle_hangup(source, condition)
  685. if self.daemon.data['class'] == 'VERSION':
  686. self.update_version(self.daemon.version)
  687. elif self.daemon.data['class'] == 'TPV':
  688. self.update_speed(self.daemon.data)
  689. elif self.nautical and self.daemon.data['class'] == 'SKY':
  690. self.update_skyview(self.daemon.data)
  691. return True
  692. def handle_hangup(self, _dummy, _unused):
  693. w = Gtk.MessageDialog(
  694. parent=self.window,
  695. message_type=Gtk.MessageType.ERROR,
  696. destroy_with_parent=True,
  697. buttons=Gtk.ButtonsType.OK
  698. )
  699. w.connect("destroy", lambda unused: Gtk.main_quit())
  700. w.set_title('gpsd error')
  701. w.set_markup("gpsd has stopped sending data.")
  702. w.run()
  703. Gtk.main_quit()
  704. return True
  705. def update_speed(self, data):
  706. if hasattr(data, 'speed'):
  707. self.widget.last_speed = data.speed
  708. self.widget.queue_draw()
  709. if self.nautical and hasattr(data, 'track'):
  710. self.widget.last_heading = data.track
  711. self.widget.queue_draw()
  712. # Used for NauticalSpeedometer only
  713. def update_skyview(self, data):
  714. "Update the satellite list and skyview."
  715. if hasattr(data, 'satellites'):
  716. self.widget.satellites = data.satellites
  717. self.widget.queue_draw()
  718. def update_version(self, ver):
  719. "Update the Version"
  720. if ver.release != gps_version:
  721. sys.stderr.write("%s: WARNING gpsd version %s different than "
  722. "expected %s\n" %
  723. (sys.argv[0], ver.release, gps_version))
  724. if ((ver.proto_major != gps.api_version_major or
  725. ver.proto_minor != gps.api_version_minor)):
  726. sys.stderr.write("%s: WARNING API version %s.%s different than "
  727. "expected %s.%s\n" %
  728. (sys.argv[0], ver.proto_major, ver.proto_minor,
  729. gps.api_version_major, gps.api_version_minor))
  730. def destroy(self, _unused, _empty=None):
  731. Gtk.main_quit()
  732. def run(self):
  733. try:
  734. daemon = gps.gps(
  735. host=self.host,
  736. port=self.port,
  737. mode=gps.WATCH_ENABLE | gps.WATCH_JSON | gps.WATCH_SCALED,
  738. verbose=self.debug
  739. )
  740. self.watch(daemon, self.device)
  741. Gtk.main()
  742. except SocketError:
  743. w = Gtk.MessageDialog(
  744. parent=self.window,
  745. message_type=Gtk.MessageType.ERROR,
  746. destroy_with_parent=True,
  747. buttons=Gtk.ButtonsType.OK
  748. )
  749. w.set_title('socket error')
  750. w.set_markup(
  751. "could not connect to gpsd socket. make sure gpsd is running."
  752. )
  753. w.run()
  754. w.destroy()
  755. except KeyboardInterrupt:
  756. pass
  757. if __name__ == '__main__':
  758. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  759. epilog = ('BSD terms apply: see the file COPYING in the distribution root'
  760. ' for details.')
  761. parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
  762. parser.add_argument(
  763. '-D',
  764. '--debug',
  765. dest='debug',
  766. default=0,
  767. type=int,
  768. help='Set level of debug. Must be integer. [Default %(default)s)]'
  769. )
  770. parser.add_argument(
  771. '--device',
  772. dest='device',
  773. default='',
  774. help='The device to connect. [Default %(default)s)]'
  775. )
  776. parser.add_argument(
  777. '--host',
  778. dest='host',
  779. default='localhost',
  780. help='The host to connect. [Default %(default)s)]'
  781. )
  782. parser.add_argument(
  783. '--landspeed',
  784. dest='nautical',
  785. default=True,
  786. action='store_false',
  787. help='Enable dashboard-style speedometer.'
  788. )
  789. parser.add_argument(
  790. '--maxspeed',
  791. dest='maxspeed',
  792. default='50',
  793. help='Max speed of the speedmeter [Default %(default)s]'
  794. )
  795. parser.add_argument(
  796. '--nautical',
  797. dest='nautical',
  798. default=True,
  799. action='store_true',
  800. help='Enable nautical-style speed and track display.'
  801. )
  802. parser.add_argument(
  803. '--port',
  804. dest='port',
  805. default=gps.GPSD_PORT,
  806. help='The port to connect. [Default %(default)s)]'
  807. )
  808. parser.add_argument(
  809. '-r',
  810. '--rotate',
  811. dest='rotate',
  812. default=0,
  813. type=float,
  814. help='Rotation of skyview ("up" direction) in degrees. '
  815. ' [Default %(default)s)]'
  816. )
  817. parser.add_argument(
  818. '--speedunits',
  819. dest='speedunits',
  820. default='mph',
  821. choices=['mph', 'kmh', 'knots'],
  822. help='The unit of speed. [Default %(default)s)]'
  823. )
  824. parser.add_argument(
  825. '-V', '--version',
  826. action='version',
  827. version="%(prog)s: Version " + gps_version + "\n",
  828. help='Output version to stderr, then exit'
  829. )
  830. parser.add_argument(
  831. 'target',
  832. nargs='?',
  833. help='[host[:port[:device]]]'
  834. )
  835. options = parser.parse_args()
  836. # the options host, port, device are set by the defaults
  837. if options.target:
  838. # override with target
  839. arg = options.target.split(':')
  840. len_arg = len(arg)
  841. if len_arg == 1:
  842. (options.host,) = arg
  843. elif len_arg == 2:
  844. (options.host, options.port) = arg
  845. elif len_arg == 3:
  846. (options.host, options.port, options.device) = arg
  847. else:
  848. parser.print_help()
  849. sys.exit(0)
  850. target = ':'.join(options.target[0:])
  851. ltarget = [options.host, options.port, options.device]
  852. target = ':'.join(ltarget)
  853. if 'DISPLAY' not in os.environ:
  854. sys.stderr.write("xgps: ERROR: DISPLAY not set\n")
  855. sys.exit(1)
  856. Main(
  857. host=options.host,
  858. port=options.port,
  859. device=options.device,
  860. speed_unit=options.speedunits,
  861. maxspeed=options.maxspeed,
  862. nautical=options.nautical,
  863. debug=options.debug,
  864. rotate=options.rotate,
  865. target=target,
  866. ).run()
  867. # vim: set expandtab shiftwidth=4