cleanup specfile for packaging
[profile/ivi/gpsd.git] / xgpsspeed
1 #!/usr/bin/env python
2 # -*- coding: utf8 -*-
3
4 import pygtk
5 pygtk.require('2.0')
6 import gtk
7 import cairo
8 import gobject
9 from math import pi
10 from math import cos
11 from math import sin
12 from socket import error as SocketError
13
14 __author__ = 'Robin Wittler <real@the-real.org>'
15 __license__ = 'BSD'
16 __version__ = '0.0.7'
17 # BSD terms apply: see the file COPYING in the distribution root for details.
18
19 #TODO
20 # add getopts and handle it
21 # add configparser
22 # add a config menu entry
23 # write unit tests!
24 # testing!
25 # cleanup and sanitize code
26
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
36         self.res_div = 10.0
37         self.res_div_mul = 1
38         self.last_speed = 0
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'
45         self.conversions = {
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
49         }
50         self.speed_unit = speed_unit or self.MPH_UNIT_LABEL
51         if not self.speed_unit in self.conversions:
52             raise TypeError(
53                     '%s is not a valid speed unit'
54                     %(repr(speed_unit))
55             )
56         self.nums = {
57                 -8:  0,
58                 -7: 10,
59                 -6: 20,
60                 -5: 30,
61                 -4: 40,
62                 -3: 50,
63                 -2: 60,
64                 -1: 70,
65                 0:  80,
66                 1:  90,
67                 2: 100
68         }
69
70
71     def expose_event(self, widget, event, data=None):
72         self.cr = self.window.cairo_create()
73         self.cr.rectangle(
74                 event.area.x,
75                 event.area.y,
76                 event.area.width,
77                 event.area.height
78         )
79         self.cr.clip()
80         x, y = self.get_x_y()
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)
87
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)
91         self.cr.fill()
92         self.cr.set_source_rgb(0.0, 0.0, 0.0)
93
94         #draw the speedometer arc
95         self.cr.arc_negative(
96                 x,
97                 y,
98                 radius,
99                 self.degrees_to_radians(60),
100                 self.degrees_to_radians(120)
101         )
102         self.cr.stroke()
103         long_inset = self.long_inset(radius)
104         middle_inset = self.middle_inset(radius)
105         short_inset = self.short_inset(radius)
106
107         #draw the ticks
108         for i in self.long_ticks:
109             self.cr.move_to(
110                     x + (radius - long_inset) * cos(i * pi / 6.0),
111                     y + (radius - long_inset) * sin(i * pi / 6.0)
112             )
113             self.cr.line_to(
114                     x + (radius + (self.cr.get_line_width() / 2)) * cos(i * pi
115                         / 6.0),
116                     y + (radius + (self.cr.get_line_width() / 2)) * sin(i * pi
117                         / 6.0)
118             )
119             self.cr.select_font_face(
120                     'Georgia',
121                     cairo.FONT_SLANT_NORMAL,
122             )
123             self.cr.set_font_size(radius / 10)
124             self.cr.save()
125             _num = str(self.nums.get(i) * self.res_div_mul)
126             (
127                     x_bearing,
128                     y_bearing,
129                     t_width,
130                     t_height,
131                     x_advance,
132                     y_advance
133             )  =  self.cr.text_extents(_num)
134
135             if i in (-8, -7, -6, -5, -4):
136                 self.cr.move_to(
137                         (x + (radius - long_inset - (t_width / 2)) * cos(i * pi
138                             / 6.0)),
139                         (y + (radius - long_inset - (t_height * 2)) * sin(i * pi
140                             / 6.0))
141                 )
142             elif i in (-2, -1, 0, 2, 1):
143                 self.cr.move_to(
144                         (x + (radius - long_inset - (t_width * 1.5 )) * cos(i * pi
145                             / 6.0)),
146                         (y + (radius - long_inset - (t_height * 2 )) * sin(i * pi
147                             / 6.0))
148                 )
149             elif i in (-3,):
150                 self.cr.move_to(
151                         (x - t_width / 2), (y - radius +
152                             self.long_inset(radius) * 2 + t_height)
153                 )
154             self.cr.show_text(_num)
155             self.cr.restore()
156
157             if i != self.long_ticks[0]:
158                 self.cr.move_to(
159                         x + (radius - middle_inset) * cos((i + 0.5) * pi / 6.0),
160                         y + (radius - middle_inset) * sin((i + 0.5) * pi / 6.0)
161                 )
162                 self.cr.line_to(
163                         x + (radius + (self.cr.get_line_width() / 2)) * cos((i
164                             + 0.5) * pi / 6.0),
165                         y + (radius + (self.cr.get_line_width() / 2)) * sin((i
166                             + 0.5) * pi / 6.0)
167                 )
168
169             for z in self.short_ticks:
170                 if i < 0:
171                     self.cr.move_to(
172                             x + (radius - short_inset) * cos((i + z) * pi / 6.0),
173                             y + (radius - short_inset) * sin((i + z) * pi / 6.0)
174                     )
175                     self.cr.line_to(
176                             x + (radius + (self.cr.get_line_width() / 2)) * cos((i
177                                 + z) * pi / 6.0),
178                             y + (radius + (self.cr.get_line_width() / 2)) * sin((i
179                                 + z) * pi / 6.0)
180                     )
181                 else:
182                     self.cr.move_to(
183                             x + (radius - short_inset) * cos((i - z) * pi / 6.0),
184                             y + (radius - short_inset) * sin((i - z) * pi / 6.0)
185                     )
186                     self.cr.line_to(
187                             x + (radius + (self.cr.get_line_width() / 2)) * cos((i
188                                 - z) * pi / 6.0),
189                             y + (radius + (self.cr.get_line_width() / 2)) * sin((i
190                                 - z) * pi / 6.0)
191                     )
192             self.cr.stroke()
193
194     def draw_needle(self, speed, radius, x, y):
195         self.cr.save()
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)
206         self.cr.line_to(
207                 x + (radius - (2 * inset)) * cos(actual * pi / 6.0),
208                 y + (radius - (2 * inset)) * sin(actual * pi / 6.0)
209         )
210         self.cr.stroke()
211         self.cr.restore()
212
213     def draw_speed_text(self, speed, radius, x, y):
214         self.cr.save()
215         speed = '%.2f %s'  %(
216                 speed * self.conversions.get(self.speed_unit),
217                 self.speed_unit
218         )
219         self.cr.select_font_face(
220                 'Georgia',
221                 cairo.FONT_SLANT_NORMAL,
222                 #cairo.FONT_WEIGHT_BOLD
223         )
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)
228         self.cr.restore()
229
230
231     def degrees_to_radians(self, degrees):
232         return ((pi / 180) * degrees)
233
234     def radians_to_degrees(self, radians):
235         return ((pi * 180) / radians)
236
237     def get_x_y(self):
238         rect = self.get_allocation()
239         x = (rect.x + rect.width / 2.0)
240         y = (rect.y + rect.height / 2.0) - 20
241         return x, y
242
243     def get_radius(self, width, height):
244         return min(width / 2.0, height / 2.0) - 20
245
246
247 class Main(object):
248     def __init__(self, host='localhost', port='2947', device=None, debug=0, speed_unit=None):
249         self.host = host
250         self.port = port
251         self.device = device
252         self.debug = debug
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)
259         self.widget.show()
260         vbox = gtk.VBox(False, 0)
261         self.window.add(vbox)
262
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')]
273         )
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',
280                      'Nautical Units', 2)
281                  ],
282                 0, lambda a, v: setattr(self.widget, 'speed_unit', ['mph',
283                 'kmh', 'knots'][a.get_current_value()])
284         )
285
286         self.uimanager.insert_action_group(self.actiongroup, 0)
287         self.uimanager.add_ui_from_string('''
288 <ui>
289     <menubar name='MenuBar'>
290         <menu action='File'>
291             <menuitem action='Quit'/>
292         </menu>
293         <menu action='Units'>
294             <menuitem action='Imperial'/>
295             <menuitem action='Metric'/>
296             <menuitem action='Nautical'/>
297         </menu>
298     </menubar>
299 </ui>
300 ''')
301         self.active_unit_map = {
302                 'mph': '/MenuBar/Units/Imperial',
303                 'kmh': '/MenuBar/Units/Metric',
304                 'knots': '/MenuBar/Units/Nautical'
305         }
306         menubar = self.uimanager.get_widget('/MenuBar')
307         self.uimanager.get_widget(
308                 self.active_unit_map.get(self.speed_unit)
309         ).set_active(True)
310         vbox.pack_start(menubar, False, False, 0)
311         vbox.add(self.widget)
312         self.window.show_all()
313
314     def watch(self, daemon, device):
315         self.daemon = daemon
316         self.device = 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)
320         return True
321
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)
327         return True
328
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
334         )
335         w.connect("destroy", lambda w: gtk.main_quit())
336         w.set_title('gpsd error')
337         w.set_markup("gpsd has stopped sending data.")
338         w.run()
339         gtk.main_quit()
340         return True
341
342     def update_speed(self, data):
343         if hasattr(data, 'speed'):
344             self.widget.last_speed = data.speed
345             self.widget.queue_draw()
346
347     def delete_event(self, widget, event, data=None):
348         #TODO handle all cleanup operations here
349         return False
350
351     def destroy(self, widget, data=None):
352         gtk.main_quit()
353
354     def run(self):
355         import gps
356         try:
357             daemon = gps.gps(
358                     host = self.host,
359                     port = self.port,
360                     mode = gps.WATCH_ENABLE|gps.WATCH_JSON|gps.WATCH_SCALED,
361                     verbose = self.debug
362             )
363             self.watch(daemon, self.device)
364             gtk.main()
365         except SocketError:
366             w = gtk.MessageDialog(
367                     type=gtk.MESSAGE_ERROR,
368                     flags=gtk.DIALOG_DESTROY_WITH_PARENT,
369                     buttons=gtk.BUTTONS_OK
370             )
371             w.set_title('socket error')
372             w.set_markup(
373                     "could not connect to gpsd socket. make sure gpsd is running."
374             )
375             w.run()
376             w.destroy()
377         except KeyboardInterrupt:
378             self.window.emit('delete_event', gtk.gdk.Event(gtk.gdk.NOTHING))
379
380
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__)
391
392     parser = OptionParser(usage=usage, epilog=epilog)
393     parser.add_option(
394             '--host',
395             dest='host',
396             default='localhost',
397             help='The host to connect. [Default localhost]'
398     )
399     parser.add_option(
400             '--port',
401             dest='port',
402             default='2947',
403             help='The port to connect. [Default 2947]'
404     )
405     parser.add_option(
406             '--device',
407             dest='device',
408             default=None,
409             help='The device to connet. [Default None]'
410     )
411     parser.add_option(
412             '--speedunits',
413             dest='speedunits',
414             default='mph',
415             help='The unit of speed. Possible units are: mph, kmh, knots. [Default mph]'
416     )
417     parser.add_option(
418             '--debug',
419             dest='debug',
420             default=0,
421             action='store',
422             type='int',
423             help='Set level of debug. Must be integer. [Default 0]'
424     )
425     parser.add_option(
426             '-V',
427             '--version',
428             action='store_true',
429             default=False,
430             dest='version',
431             help='show program\'s version number and exit'
432     )
433     (options, args) = parser.parse_args()
434     if options.version:
435         print version
436         exit(0)
437     if args:
438         arg = args[0].split(':')
439         len_arg = len(arg)
440         if len_arg == 1:
441             (options.host,) = arg
442         elif len_arg == 2:
443             (options.host, options.port) = arg
444         elif len_arg == 3:
445             (options.host, options.port, options.device) = arg
446         else:
447             parser.print_help()
448             exit(0)
449     Main(
450             host=options.host,
451             port=options.port,
452             device=options.device,
453             speed_unit=options.speedunits,
454             debug=options.debug
455     ).run()