xgpsspeed.py.in 35 KB

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