12 from socket import error as SocketError
14 __author__ = 'Robin Wittler <real@the-real.org>'
17 # BSD terms apply: see the file COPYING in the distribution root for details.
20 # add getopts and handle it
22 # add a config menu entry
25 # cleanup and sanitize code
27 class Speedometer(gtk.DrawingArea):
28 def __init__(self, speed_unit=None):
29 gtk.DrawingArea.__init__(self)
30 self.connect('expose_event', self.expose_event)
31 self.long_ticks = (2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8)
32 self.short_ticks = (0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9)
33 self.long_inset = lambda x: 0.1 * x
34 self.middle_inset = lambda x: self.long_inset(x) / 1.5
35 self.short_inset = lambda x: self.long_inset(x) / 3
39 self.MPS_TO_KPH = 3.6000000000000001
40 self.MPS_TO_MPH = 2.2369363
41 self.MPS_TO_KNOTS = 1.9438445
42 self.MPH_UNIT_LABEL = 'mph'
43 self.KPH_UNIT_LABEL = 'kmh'
44 self.KNOTS_UNIT_LABEL = 'knots'
46 self.MPH_UNIT_LABEL: self.MPS_TO_MPH,
47 self.KPH_UNIT_LABEL: self.MPS_TO_KPH,
48 self.KNOTS_UNIT_LABEL: self.MPS_TO_KNOTS
50 self.speed_unit = speed_unit or self.MPH_UNIT_LABEL
51 if not self.speed_unit in self.conversions:
53 '%s is not a valid speed unit'
71 def expose_event(self, widget, event, data=None):
72 self.cr = self.window.cairo_create()
81 width, height = self.window.get_size()
82 radius = self.get_radius(width, height)
83 self.cr.set_line_width(radius / 100)
84 self.draw_arc_and_ticks(width, height, radius, x, y)
85 self.draw_needle(self.last_speed, radius, x, y)
86 self.draw_speed_text(self.last_speed, radius, x, y)
88 def draw_arc_and_ticks(self, width, height, radius, x, y):
89 self.cr.set_source_rgb(1.0, 1.0, 1.0)
90 self.cr.rectangle(0, 0, width, height)
92 self.cr.set_source_rgb(0.0, 0.0, 0.0)
94 #draw the speedometer arc
99 self.degrees_to_radians(60),
100 self.degrees_to_radians(120)
103 long_inset = self.long_inset(radius)
104 middle_inset = self.middle_inset(radius)
105 short_inset = self.short_inset(radius)
108 for i in self.long_ticks:
110 x + (radius - long_inset) * cos(i * pi / 6.0),
111 y + (radius - long_inset) * sin(i * pi / 6.0)
114 x + (radius + (self.cr.get_line_width() / 2)) * cos(i * pi
116 y + (radius + (self.cr.get_line_width() / 2)) * sin(i * pi
119 self.cr.select_font_face(
121 cairo.FONT_SLANT_NORMAL,
123 self.cr.set_font_size(radius / 10)
125 _num = str(self.nums.get(i) * self.res_div_mul)
133 ) = self.cr.text_extents(_num)
135 if i in (-8, -7, -6, -5, -4):
137 (x + (radius - long_inset - (t_width / 2)) * cos(i * pi
139 (y + (radius - long_inset - (t_height * 2)) * sin(i * pi
142 elif i in (-2, -1, 0, 2, 1):
144 (x + (radius - long_inset - (t_width * 1.5 )) * cos(i * pi
146 (y + (radius - long_inset - (t_height * 2 )) * sin(i * pi
151 (x - t_width / 2), (y - radius +
152 self.long_inset(radius) * 2 + t_height)
154 self.cr.show_text(_num)
157 if i != self.long_ticks[0]:
159 x + (radius - middle_inset) * cos((i + 0.5) * pi / 6.0),
160 y + (radius - middle_inset) * sin((i + 0.5) * pi / 6.0)
163 x + (radius + (self.cr.get_line_width() / 2)) * cos((i
165 y + (radius + (self.cr.get_line_width() / 2)) * sin((i
169 for z in self.short_ticks:
172 x + (radius - short_inset) * cos((i + z) * pi / 6.0),
173 y + (radius - short_inset) * sin((i + z) * pi / 6.0)
176 x + (radius + (self.cr.get_line_width() / 2)) * cos((i
178 y + (radius + (self.cr.get_line_width() / 2)) * sin((i
183 x + (radius - short_inset) * cos((i - z) * pi / 6.0),
184 y + (radius - short_inset) * sin((i - z) * pi / 6.0)
187 x + (radius + (self.cr.get_line_width() / 2)) * cos((i
189 y + (radius + (self.cr.get_line_width() / 2)) * sin((i
194 def draw_needle(self, speed, radius, x, y):
196 inset = self.long_inset(radius)
197 speed = speed * self.conversions.get(self.speed_unit)
198 speed = speed / (self.res_div * self.res_div_mul)
199 actual = self.long_ticks[-1] + speed
200 if actual > self.long_ticks[0]:
201 #TODO test this in real conditions! ;)
202 self.res_div_mul += 1
203 speed = speed / (self.res_div * self.res_div_mul)
204 actual = self.long_ticks[-1] + speed
205 self.cr.move_to(x, y)
207 x + (radius - (2 * inset)) * cos(actual * pi / 6.0),
208 y + (radius - (2 * inset)) * sin(actual * pi / 6.0)
213 def draw_speed_text(self, speed, radius, x, y):
216 speed * self.conversions.get(self.speed_unit),
219 self.cr.select_font_face(
221 cairo.FONT_SLANT_NORMAL,
222 #cairo.FONT_WEIGHT_BOLD
224 self.cr.set_font_size(radius / 10)
225 x_bearing, y_bearing, t_width, t_height = self.cr.text_extents(speed)[:4]
226 self.cr.move_to((x - t_width / 2), (y + radius) - self.long_inset(radius))
227 self.cr.show_text(speed)
231 def degrees_to_radians(self, degrees):
232 return ((pi / 180) * degrees)
234 def radians_to_degrees(self, radians):
235 return ((pi * 180) / radians)
238 rect = self.get_allocation()
239 x = (rect.x + rect.width / 2.0)
240 y = (rect.y + rect.height / 2.0) - 20
243 def get_radius(self, width, height):
244 return min(width / 2.0, height / 2.0) - 20
248 def __init__(self, host='localhost', port='2947', device=None, debug=0, speed_unit=None):
253 self.speed_unit = speed_unit
254 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
255 self.window.set_title('xgpsspeed')
256 self.widget = Speedometer(speed_unit=self.speed_unit)
257 self.window.connect('delete_event', self.delete_event)
258 self.window.connect('destroy', self.destroy)
260 vbox = gtk.VBox(False, 0)
261 self.window.add(vbox)
263 self.window.present()
264 self.uimanager = gtk.UIManager()
265 self.accelgroup = self.uimanager.get_accel_group()
266 self.window.add_accel_group(self.accelgroup)
267 self.actiongroup = gtk.ActionGroup('gpsspeed-ng')
268 self.actiongroup.add_actions(
269 [('Quit', gtk.STOCK_QUIT, '_Quit', None,
270 'Quit the Program', lambda x: gtk.main_quit()),
271 ('File', None, '_File'),
272 ('Units', None, '_Units')]
274 self.actiongroup.add_radio_actions(
275 [('Imperial', None, '_Imperial', '<Control>i',
276 'Imperial Units', 0),
277 ('Metric', None, '_Metric', '<Control>m',
278 'Metrical Units', 1),
279 ('Nautical', None, '_Nautical', '<Control>n',
282 0, lambda a, v: setattr(self.widget, 'speed_unit', ['mph',
283 'kmh', 'knots'][a.get_current_value()])
286 self.uimanager.insert_action_group(self.actiongroup, 0)
287 self.uimanager.add_ui_from_string('''
289 <menubar name='MenuBar'>
291 <menuitem action='Quit'/>
293 <menu action='Units'>
294 <menuitem action='Imperial'/>
295 <menuitem action='Metric'/>
296 <menuitem action='Nautical'/>
301 self.active_unit_map = {
302 'mph': '/MenuBar/Units/Imperial',
303 'kmh': '/MenuBar/Units/Metric',
304 'knots': '/MenuBar/Units/Nautical'
306 menubar = self.uimanager.get_widget('/MenuBar')
307 self.uimanager.get_widget(
308 self.active_unit_map.get(self.speed_unit)
310 vbox.pack_start(menubar, False, False, 0)
311 vbox.add(self.widget)
312 self.window.show_all()
314 def watch(self, daemon, device):
317 gobject.io_add_watch(daemon.sock, gobject.IO_IN, self.handle_response)
318 gobject.io_add_watch(daemon.sock, gobject.IO_ERR, self.handle_hangup)
319 gobject.io_add_watch(daemon.sock, gobject.IO_HUP, self.handle_hangup)
322 def handle_response(self, source, condition):
323 if self.daemon.poll() == -1:
324 self.handle_hangup(source, condition)
325 if self.daemon.data['class'] == 'TPV':
326 self.update_speed(self.daemon.data)
329 def handle_hangup(self, source, condition):
330 w = gtk.MessageDialog(
331 type=gtk.MESSAGE_ERROR,
332 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
333 buttons=gtk.BUTTONS_OK
335 w.connect("destroy", lambda w: gtk.main_quit())
336 w.set_title('gpsd error')
337 w.set_markup("gpsd has stopped sending data.")
342 def update_speed(self, data):
343 if hasattr(data, 'speed'):
344 self.widget.last_speed = data.speed
345 self.widget.queue_draw()
347 def delete_event(self, widget, event, data=None):
348 #TODO handle all cleanup operations here
351 def destroy(self, widget, data=None):
360 mode = gps.WATCH_ENABLE|gps.WATCH_JSON|gps.WATCH_SCALED,
363 self.watch(daemon, self.device)
366 w = gtk.MessageDialog(
367 type=gtk.MESSAGE_ERROR,
368 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
369 buttons=gtk.BUTTONS_OK
371 w.set_title('socket error')
373 "could not connect to gpsd socket. make sure gpsd is running."
377 except KeyboardInterrupt:
378 self.window.emit('delete_event', gtk.gdk.Event(gtk.gdk.NOTHING))
381 if __name__ == '__main__':
382 from sys import argv, exit
383 from os.path import basename
384 from optparse import OptionParser
385 prog = basename(argv[0])
386 usage = ('%s [-V|--version] [-h|--help] [--debug] [--host] ' +
387 '[--port] [--device] [--speedunits {[mph] [kmh] [knots]}] ' +
388 '[host [:port [:device]]]') %(prog)
389 epilog = 'BSD terms apply: see the file COPYING in the distribution root for details.'
390 version = '%s %s' %(prog, __version__)
392 parser = OptionParser(usage=usage, epilog=epilog)
397 help='The host to connect. [Default localhost]'
403 help='The port to connect. [Default 2947]'
409 help='The device to connet. [Default None]'
415 help='The unit of speed. Possible units are: mph, kmh, knots. [Default mph]'
423 help='Set level of debug. Must be integer. [Default 0]'
431 help='show program\'s version number and exit'
433 (options, args) = parser.parse_args()
438 arg = args[0].split(':')
441 (options.host,) = arg
443 (options.host, options.port) = arg
445 (options.host, options.port, options.device) = arg
452 device=options.device,
453 speed_unit=options.speedunits,