cleanup specfile for packaging
[profile/ivi/gpsd.git] / xgps
1 #!/usr/bin/env python
2 '''
3 xgps -- test client for gpsd
4
5 usage: xgps [-D level] [-hV?] [-l degmfmt] [-u units] [server[:port[:device]]]
6 '''
7
8 gui_about = '''\
9 This is xgps, a test client for the gpsd daemon.
10
11 By Eric S. Raymond for the GPSD project, December 2009
12 '''
13 #
14 # This file is Copyright (c) 2010 by the GPSD project
15 # BSD terms apply: see the file COPYING in the distribution root for details.
16
17 import sys, os, re, math, time, exceptions, getopt, socket
18
19 import gobject, pygtk
20 pygtk.require('2.0')
21 import gtk
22
23 import gps, gps.clienthelpers
24
25 class unit_adjustments:
26     "Encapsulate adjustments for unit systems."
27     def __init__(self, units=None):
28         self.altfactor = gps.METERS_TO_FEET
29         self.altunits = "ft"
30         self.speedfactor = gps.MPS_TO_MPH
31         self.speedunits = "mph"
32         if units is None:
33             units = gps.clienthelpers.gpsd_units()
34         if units in (gps.clienthelpers.unspecified, gps.clienthelpers.imperial, "imperial", "i"):
35             pass
36         elif units in (gps.clienthelpers.nautical, "nautical", "n"):
37             self.altfactor = gps.METERS_TO_FEET
38             self.altunits = "ft"
39             self.speedfactor = gps.MPS_TO_KNOTS
40             self.speedunits = "knots"
41         elif units in (gps.clienthelpers.metric, "metric", "m"):
42             self.altfactor = 1
43             self.altunits = "m"
44             self.speedfactor = gps.MPS_TO_KPH
45             self.speedunits = "kph"
46         else:
47             raise ValueError    # Should never happen
48
49 class SkyView(gtk.DrawingArea):
50     "Satellite skyview, encapsulates pygtk's draw-on-expose behavior."
51     # See <http://faq.pygtk.org/index.py?req=show&file=faq18.008.htp>
52     HORIZON_PAD = 20    # How much whitespace to leave around horizon
53     SAT_RADIUS = 5      # Diameter of satellite circle
54     GPS_PRNMAX = 32     # above this number are SBAS satellites
55     def __init__(self):
56         gtk.DrawingArea.__init__(self)
57         self.set_size_request(400, 400)
58         self.gc = None  # initialized in realize-event handler
59         self.width  = 0 # updated in size-allocate handler
60         self.height = 0 # updated in size-allocate handler
61         self.connect('size-allocate', self.on_size_allocate)
62         self.connect('expose-event',  self.on_expose_event)
63         self.connect('realize',       self.on_realize)
64         self.pangolayout = self.create_pango_layout("")
65         self.satellites = []
66
67     def on_realize(self, widget):
68         self.gc = widget.window.new_gc()
69         self.gc.set_line_attributes(1, gtk.gdk.LINE_SOLID,
70                                     gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
71
72     def on_size_allocate(self, widget, allocation):
73         self.width = allocation.width
74         self.height = allocation.height
75         self.diameter = min(self.width, self.height) - SkyView.HORIZON_PAD
76
77     def set_color(self, spec):
78         "Set foreground color for draweing."
79         self.gc.set_rgb_fg_color(gtk.gdk.color_parse(spec))
80
81     def draw_circle(self, widget, x, y, diam, filled=False):
82         "Draw a circle centered on the specified midpoint."
83         widget.window.draw_arc(self.gc, filled,
84                                x - diam / 2, y - diam / 2,
85                                diam, diam, 0, 360 * 64)
86
87     def draw_line(self, widget, x1, y1, x2, y2):
88         "Draw a line between specified points."
89         widget.window.draw_lines(self.gc, [(x1, y1), (x2, y2)])
90
91     def draw_square(self, widget, x, y, diam, filled=False):
92         "Draw a square centered on the specified midpoint."
93         widget.window.draw_rectangle(self.gc, filled,
94                                      x - diam / 2, y - diam / 2,
95                                      diam, diam)
96
97     def draw_string(self, widget, x, y, letter, centered=True):
98         "Draw a letter on the skyview."
99         self.pangolayout.set_text(letter)
100         if centered:
101             (w, h) = self.pangolayout.get_pixel_size()
102             x -= w/2
103             y -= h/2
104         self.window.draw_layout(self.gc, x, y, self.pangolayout)
105
106     def pol2cart(self, az, el):
107         "Polar to Cartesian coordinates within the horizon circle."
108         az *= (math.pi/180)     # Degrees to radians
109         # Exact spherical projection would be like this:
110         # el = sin((90.0 - el) * DEG_2_RAD);
111         el = ((90.0 - el) / 90.0);
112         xout = int((self.width / 2) + math.sin(az) * el * (self.diameter / 2))
113         yout = int((self.height / 2) - math.cos(az) * el * (self.diameter / 2))
114         return (xout, yout)
115
116     def on_expose_event(self, widget, event):
117         self.set_color("white")
118         widget.window.draw_rectangle(self.gc, True, 0,0, self.width,self.height)
119         # The zenith marker
120         self.set_color("gray")
121         self.draw_circle(widget, self.width / 2, self.height / 2, 6)
122         # The circle corresponding to 45 degrees elevation.
123         # There are two ways we could plot this.  Projecting the sphere
124         # on the display plane, the circle would have a diameter of
125         # sin(45) ~ 0.7.  But the naive linear mapping, just splitting
126         # the horizon diameter in half, seems to work better visually.
127         self.draw_circle(widget, self.width / 2, self.height / 2,
128                          int(self.diameter * 0.5))
129         self.set_color("black")
130         # The horizon circle
131         self.draw_circle(widget, self.width / 2, self.height / 2,
132                          self.diameter)
133         self.set_color("gray")
134         (x1, y1) = self.pol2cart(0, 0)
135         (x2, y2) = self.pol2cart(180, 0)
136         self.draw_line(widget, x1, y1, x2, y2)
137         (x1, y1) = self.pol2cart(90, 0)
138         (x2, y2) = self.pol2cart(270, 0)
139         self.draw_line(widget, x1, y1, x2, y2)
140         # The compass-point letters
141         self.set_color("black")
142         (x, y) = self.pol2cart(0, 0)
143         self.draw_string(widget, x, y+10, "N")
144         (x, y) = self.pol2cart(90, 0)
145         self.draw_string(widget, x-10, y, "E")
146         (x, y) = self.pol2cart(180, 0)
147         self.draw_string(widget, x, y-10, "S")
148         (x, y) = self.pol2cart(270, 0)
149         self.draw_string(widget, x+10, y, "W")
150         # The satellites
151         for sat in self.satellites:
152             (x, y) = self.pol2cart(sat.az, sat.el)
153             if sat.ss < 10:
154                 self.set_color("Black")
155             elif sat.ss < 30:
156                 self.set_color("Red")
157             elif sat.ss < 35:
158                 self.set_color("Yellow");
159             elif sat.ss < 40:
160                 self.set_color("Green3");
161             else:
162                 self.set_color("Green1");
163             if sat.PRN > SkyView.GPS_PRNMAX:
164                 self.draw_square(widget,
165                                  x-SkyView.SAT_RADIUS, y-SkyView.SAT_RADIUS,
166                                  2 * SkyView.SAT_RADIUS + 1, sat.used);
167             else:
168                 self.draw_circle(widget,
169                                  x-SkyView.SAT_RADIUS, y-SkyView.SAT_RADIUS,
170                                  2 * SkyView.SAT_RADIUS + 1, sat.used);
171             self.set_color("Black")
172             self.draw_string(widget, x, y, str(sat.PRN), centered=False)
173     def redraw(self, satellites):
174         "Redraw the skyview."
175         self.satellites = satellites
176         self.queue_draw()
177
178 class AISView:
179     "Encapsulate store and view objects for watching AIS data." 
180     AIS_ENTRIES = 10
181     DWELLTIME = 360
182     def __init__(self, deg_type):
183         "Initialize the store and view."
184         self.deg_type = deg_type
185         self.name_to_mmsi = {}
186         self.named = {}
187         self.store = gtk.ListStore(str,str,str,str,str,str)
188         self.widget = gtk.ScrolledWindow()
189         self.widget.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
190         self.view = gtk.TreeView(model=self.store)
191         self.widget.set_size_request(-1, 300)
192         self.widget.add_with_viewport(self.view)
193
194         for (i, label) in enumerate(('#', 'Name:','Callsign:','Destination:', "Lat/Lon:", "Information")):
195             column = gtk.TreeViewColumn(label)
196             renderer = gtk.CellRendererText()
197             column.pack_start(renderer)
198             column.add_attribute(renderer, 'text', i)
199             self.view.append_column(column)
200
201     def enter(self, ais, name):
202         "Add a named object (ship or station) to the store."
203         if ais.mmsi in self.named:
204             return False
205         else:
206             ais.entry_time = time.time()
207             self.named[ais.mmsi] = ais
208             self.name_to_mmsi[name] = ais.mmsi
209             # Garbage-collect old entries
210             try:
211                 for i in range(len(self.store)):
212                     here = self.store.get_iter(i)
213                     name = self.store.get_value(here, 1)
214                     mmsi = self.name_to_mmsi[name]
215                     if self.named[mmsi].entry_time < time.time() - AISView.DWELLTIME:
216                         del self.named[mmsi]
217                         if name in self.name_to_mmsi:
218                             del self.name_to_mmsi[name]
219                         self.store.remove(here)
220             except (ValueError, KeyError):      # Invalid TreeIters throw these
221                 pass
222             return True
223
224     def latlon(self, lat, lon):
225         "Latitude/longitude display in nice format."
226         if lat < 0:
227             latsuff = "S"
228         elif lat > 0:
229             latsuff = "N"
230         else:
231             latsuff = ""
232         lat = abs(lat)
233         lat = gps.clienthelpers.deg_to_str(self.deg_type, lat)
234         if lon < 0:
235             lonsuff = "W"
236         elif lon > 0:
237             lonsuff = "E"
238         else:
239             lonsuff = ""
240         lon = abs(lon)
241         lon = gps.clienthelpers.deg_to_str(gps.clienthelpers.deg_ddmmss, lon)
242         return lat + latsuff + "/" + lon + lonsuff
243
244     def update(self, ais):
245         "Update the AIS data fields."
246         if ais.type in (1, 2, 3, 18):
247             if ais.mmsi in self.named:
248                 for i in range(len(self.store)):
249                     here = self.store.get_iter(i)
250                     name = self.store.get_value(here, 1)
251                     if name in self.name_to_mmsi:
252                         mmsi = self.name_to_mmsi[name]
253                         if mmsi == ais.mmsi:
254                             latlon = self.latlon(ais.lat, ais.lon)
255                             self.store.set_value(here, 4, latlon)
256         elif ais.type == 4:
257             if self.enter(ais, ais.mmsi):
258                 where = self.latlon(ais.lat, ais.lon)
259                 self.store.prepend(
260                     (ais.type, ais.mmsi, "(shore)", ais.timestamp, where, ais.epfd))
261         elif ais.type == 5:
262             if self.enter(ais, ais.shipname):
263                 self.store.prepend(
264                     (ais.type, ais.shipname, ais.callsign, ais.destination, "", ais.shiptype))
265         elif ais.type == 12:
266             sender = ais.mmsi
267             if sender in self.named:
268                 sender = self.named[sender].shipname
269             recipient = ais.dest_mmsi
270             if recipient in self.named and hasattr(self.named[recipient], "shipname"):
271                 recipient = self.named[recipient].shipname
272             self.store.prepend(
273                 (ais.type, sender, "", recipient, "", ais.text))
274         elif ais.type == 14:
275             sender = ais.mmsi
276             if sender in self.named:
277                 sender = self.named[sender].shipname
278             self.store.prepend(
279                 (ais.type, sender, "", "(broadcast)", "", ais.text))
280         elif ais.type in (19, 24):
281             if self.enter(ais, ais.shipname):
282                 self.store.prepend(
283                     (ais.type, ais.shipname, "(class B)", "", "", ais.shiptype))
284         elif ais.type == 21:
285             if self.enter(ais, ais.name):
286                 where = self.latlon(ais.lat, ais.lon)
287                 self.store.prepend(
288                     (ais.type, ais.name, "(%s navaid)" % ais.epfd, "", where, ais.aid_type))
289                     
290 class Base:
291     gpsfields = (
292         # First column
293         ("Time", lambda s, r: s.update_time(r)),
294         ("Latitude", lambda s, r: s.update_latitude(r)),
295         ("Longitude", lambda s, r: s.update_longitude(r)),
296         ("Altitude", lambda s, r: s.update_altitude(r)),
297         ("Speed", lambda s, r: s.update_speed(r)),
298         ("Climb", lambda s, r: s.update_climb(r)),
299         ("Track", lambda s, r: s.update_track(r)),
300         # Second column
301         ("Status", lambda s, r: s.update_status(r)),
302         ("EPX", lambda s, r: s.update_err(r, "epx")),
303         ("EPY", lambda s, r: s.update_err(r, "epy")),
304         ("EPV", lambda s, r: s.update_err(r, "epv")),
305         ("EPS", lambda s, r: s.update_err(r, "eps")),
306         ("EPC", lambda s, r: s.update_err(r, "epc")),
307         ("EPD", lambda s, r: s.update_err(r, "epd")),
308         )
309     def __init__(self, deg_type):
310         self.deg_type = deg_type
311         self.conversions = unit_adjustments()
312         self.saved_mode = -1
313         self.ais_latch = False
314   
315         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
316         self.window.set_title("xgps")
317         self.window.connect("delete_event", self.delete_event)
318         self.window.set_resizable(False)
319
320         vbox = gtk.VBox(False, 0)
321         self.window.add(vbox)
322
323         self.window.connect("destroy", lambda w: gtk.main_quit())
324
325         self.uimanager = gtk.UIManager()
326         self.accelgroup = self.uimanager.get_accel_group()
327         self.window.add_accel_group(self.accelgroup)
328         self.actiongroup = gtk.ActionGroup('xgps')
329         self.actiongroup.add_actions(
330             [('Quit', gtk.STOCK_QUIT, '_Quit', None,
331               'Quit the Program', lambda w: gtk.main_quit()),
332              ('File', None, '_File'),
333              ('View', None, '_View'),
334              ('Units', None, '_Units')])
335         self.actiongroup.add_toggle_actions(
336             [('Skyview', None, '_Skyview', '<Control>s',
337               'Enable Skyview', lambda a: self.view_toggle(a)),
338              ('Responses', None, '_Responses', '<Control>r',
339               'Enable Response Reports', lambda a: self.view_toggle(a)),
340              ('GPS', None, '_GPS Data', '<Control>g',
341               'Enable GPS Data', lambda a: self.view_toggle(a)),
342              ('AIS', None, '_AIS Data', '<Control>a',
343               'Enable AIS Data', lambda a: self.view_toggle(a)),
344              ])
345         self.actiongroup.add_radio_actions(
346             [('Imperial', None, '_Imperial', '<Control>i',
347               'Imperial units', 0),
348              ('Nautical', None, '_Nautical', '<Control>n',
349               'Nautical units', 1),
350              ('Metric', None, '_Metric', '<Control>m',
351               'Metric Units', 2),
352              ], 0, lambda a, v: self.set_units(['i', 'n', 'm'][a.get_current_value()]))
353         self.uimanager.insert_action_group(self.actiongroup, 0)
354         self.uimanager.add_ui_from_string('''
355 <ui>
356     <menubar name="MenuBar">
357       <menu action="File">
358         <menuitem action="Quit"/>
359       </menu>
360       <menu action="View">
361         <menuitem action="Skyview"/>
362         <menuitem action="Responses"/>
363         <menuitem action="GPS"/>
364         <menuitem action="AIS"/>
365       </menu>
366       <menu action="Units">
367         <menuitem action="Imperial"/>
368         <menuitem action="Nautical"/>
369         <menuitem action="Metric"/>
370       </menu>
371     </menubar>
372 </ui>
373 ''')
374         self.uimanager.get_widget('/MenuBar/View/Skyview').set_active(True)
375         self.uimanager.get_widget('/MenuBar/View/Responses').set_active(True)
376         self.uimanager.get_widget('/MenuBar/View/GPS').set_active(True)
377         self.uimanager.get_widget('/MenuBar/View/AIS').set_active(True)
378         menubar = self.uimanager.get_widget('/MenuBar')
379         vbox.pack_start(menubar, False)
380
381         self.satbox = gtk.HBox(False, 0)
382         vbox.add(self.satbox)
383
384         skyframe = gtk.Frame(label="Satellite List")
385         self.satbox.add(skyframe)
386
387         self.satlist = gtk.ListStore(str,str,str,str,str)
388         view = gtk.TreeView(model=self.satlist)
389
390         for (i, label) in enumerate(('PRN:','Elev:','Azim:','SNR:','Used:')):
391             column = gtk.TreeViewColumn(label)
392             renderer = gtk.CellRendererText()
393             column.pack_start(renderer)
394             column.add_attribute(renderer, 'text', i)
395             view.append_column(column)
396
397         self.row_iters = []
398         for i in range(gps.MAXCHANNELS):
399             self.satlist.append(["", "", "", "", ""])
400             self.row_iters.append(self.satlist.get_iter(i))
401
402         skyframe.add(view)
403
404         viewframe = gtk.Frame(label="Skyview")
405         self.satbox.add(viewframe)
406         self.skyview = SkyView()
407         viewframe.add(self.skyview)
408
409         self.rawdisplay = gtk.Entry()
410         self.rawdisplay.set_editable(False)
411         vbox.add(self.rawdisplay)
412
413         self.dataframe = gtk.Frame(label="GPS data")
414         datatable = gtk.Table(7, 4, False)
415         self.dataframe.add(datatable)
416         gpswidgets = []
417         for i in range(len(Base.gpsfields)):
418             if i < len(Base.gpsfields) / 2:
419                 colbase = 0
420             else:
421                 colbase = 2
422             label = gtk.Label(Base.gpsfields[i][0] + ": ")
423             # Wacky way to force right alignment 
424             label.set_alignment(xalign=1, yalign=0.5)
425             datatable.attach(label, colbase, colbase+1, i % 7, i % 7 + 1)
426             entry = gtk.Entry()
427             datatable.attach(entry, colbase+1, colbase+2, i % 7, i % 7 + 1)
428             gpswidgets.append(entry)
429         vbox.add(self.dataframe)
430
431         self.aisbox = gtk.HBox(False, 0)
432         vbox.add(self.aisbox)
433
434         aisframe = gtk.Frame(label="AIS Data")
435         self.aisbox.add(aisframe)
436
437         self.aisview = AISView(self.deg_type)
438         aisframe.add(self.aisview.widget)
439
440         self.window.show_all()
441         # Hide the AIS window util user selects it.
442         self.uimanager.get_widget('/MenuBar/View/AIS').set_active(False)
443         self.aisbox.hide()
444
445         self.view_name_to_widget = \
446                                  {"Skyview": self.satbox,
447                                   "Responses": self.rawdisplay,
448                                   "GPS": self.dataframe,
449                                   "AIS": self.aisbox}
450
451         # Discard field labels and associate data hooks with their widgets
452         Base.gpsfields = map(lambda ((label, hook), widget): (hook, widget),
453                              zip(Base.gpsfields, gpswidgets))
454
455     def view_toggle(self, action):
456         #print "View toggle:", action.get_active(), action.get_name()
457         if hasattr(self, 'view_name_to_widget'):
458             if action.get_active():
459                 self.view_name_to_widget[action.get_name()].show()
460             else:
461                 self.view_name_to_widget[action.get_name()].hide()
462         # The effect we're after is to make the top-level window
463         # resize itself to fit when we show or hide widgets.
464         # This is undocumented magic to do that.
465         self.window.resize(1, 1)
466
467     def set_satlist_field(self, row, column, value):
468         "Set a specified field in the satellite list."
469         try:
470             self.satlist.set_value(self.row_iters[row], column, value)
471         except IndexError:
472             sys.stderr.write("xgps: channel = %d, MAXCHANNELS = %d\n" % (row, gps.MAXCHANNELS))
473
474     def delete_event(self, widget, event, data=None):
475         gtk.main_quit()
476         return False
477
478     # State updates
479
480     def update_time(self, data):
481         if hasattr(data, "time"):
482             return gps.isotime(data.time)
483         else:
484             return "n/a"
485
486     def update_latitude(self, data):
487         if data.mode >= gps.MODE_2D:
488             lat = gps.clienthelpers.deg_to_str(self.deg_type, abs(data.lat))
489             if data.lat < 0:
490                 ns = 'S'
491             else:
492                 ns = 'N'
493             return "%s %s" % (lat, ns)
494         else:
495             return "n/a"
496
497     def update_longitude(self, data):
498         if data.mode >= gps.MODE_2D:
499             lon = gps.clienthelpers.deg_to_str(self.deg_type, abs(data.lon))
500             if data.lon < 0:
501                 ew = 'W'
502             else:
503                 ew = 'E'
504             return "%s %s" % (lon, ew)
505         else:
506             return "n/a"
507
508     def update_altitude(self, data):
509         if data.mode >= gps.MODE_3D:
510             return "%.3f %s" % (
511                 data.alt * self.conversions.altfactor,
512                 self.conversions.altunits)
513         else:
514             return "n/a"
515
516     def update_speed(self, data):
517         if hasattr(data, "speed"):
518             return "%.3f %s" % (
519                 data.speed * self.conversions.speedfactor,
520                 self.conversions.speedunits)
521         else:
522             return "n/a"
523
524     def update_climb(self, data):
525         if hasattr(data, "climb"):
526             return "%.3f %s" % (
527                 data.climb * self.conversions.speedfactor,
528                 self.conversions.speedunits)
529         else:
530             return "n/a"
531
532     def update_track(self, data):
533         if hasattr(data, "track"):
534             return gps.clienthelpers.deg_to_str(self.deg_type, abs(data.track))
535         else:
536             return "n/a"
537
538     def update_err(self, data, errtype):
539         if hasattr(data, errtype):
540             return "%.3f %s" % (
541                 getattr(data, errtype) * self.conversions.altfactor,
542                 self.conversions.altunits)
543         else:
544             return "n/a"
545
546     def update_status(self, data):
547         if data.mode == gps.MODE_2D:
548             status = "2D FIX"
549         elif data.mode == gps.MODE_3D:
550             status = "3D FIX"
551         else:
552             status = "NO FIX"
553         if data.mode != self.saved_mode:
554             self.last_transition = time.time()
555             self.saved_mode = data.mode
556         return status + " (%d secs)" % (time.time() - self.last_transition)
557
558     def update_gpsdata(self, tpv):
559         "Update the GPS data fields."
560         for (hook, widget) in Base.gpsfields:
561             if hook:    # Remove this guard when we have all hooks 
562                 widget.set_text(hook(self, tpv))
563
564     def update_skyview(self, data):
565         "Update the satellite list and skyview."
566         satellites = data.satellites
567         for (i, satellite) in enumerate(satellites): 
568             self.set_satlist_field(i, 0, satellite.PRN)
569             self.set_satlist_field(i, 1, satellite.el)
570             self.set_satlist_field(i, 2, satellite.az)
571             self.set_satlist_field(i, 3, satellite.ss)
572             yesno = 'N'
573             if satellite.used:
574                 yesno = 'Y'
575             self.set_satlist_field(i, 4, yesno)
576         for i in range(len(satellites), gps.MAXCHANNELS):
577             for j in range(0, 5):
578                 self.set_satlist_field(i, j, "")
579         self.skyview.redraw(satellites)
580
581     # Preferences
582
583     def set_units(self, system):
584         "Change the display units."
585         self.conversions = unit_adjustments(system)
586
587     # I/O monitoring and gtk housekeeping
588
589     def watch(self, daemon, device):
590         "Set up monitoring of a daemon instance."
591         self.daemon = daemon
592         self.device = device
593         gobject.io_add_watch(daemon.sock, gobject.IO_IN, self.handle_response)
594         gobject.io_add_watch(daemon.sock, gobject.IO_ERR, self.handle_hangup)
595         gobject.io_add_watch(daemon.sock, gobject.IO_HUP, self.handle_hangup)
596
597     def handle_response(self, source, condition):
598         "Handle ordinary I/O ready condition from the daemon."
599         if self.daemon.poll() == -1:
600             self.handle_hangup(source, condition)
601         if self.daemon.valid & gps.PACKET_SET:
602             if self.device and self.device != self.daemon.data["device"]:
603                 return True
604             self.rawdisplay.set_text(self.daemon.response.strip())
605             if self.daemon.data["class"] == "SKY":
606                 self.update_skyview(self.daemon.data)
607             elif self.daemon.data["class"] == "TPV":
608                 self.update_gpsdata(self.daemon.data)
609             elif self.daemon.data["class"] == "AIS":
610                 self.aisview.update(self.daemon.data)
611                 if self.ais_latch == False:
612                     self.ais_latch = True
613                     self.uimanager.get_widget('/MenuBar/View/AIS').set_active(True)
614                     self.aisbox.show()
615                     
616         return True
617
618     def handle_hangup(self, source, condition):
619         "Handle hangup condition from the daemon."
620         w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
621                               flags=gtk.DIALOG_DESTROY_WITH_PARENT,
622                               buttons=gtk.BUTTONS_CANCEL)
623         w.connect("destroy", lambda w: gtk.main_quit())
624         w.set_markup("gpsd has stopped sending data.")
625         w.run()
626         gtk.main_quit()
627         return True
628
629     def main(self):
630         gtk.main()
631
632 if __name__ == "__main__":
633     (options, arguments) = getopt.getopt(sys.argv[1:], "D:hl:u:V?",
634                                          ['verbose'])
635     debug = 0
636     degreefmt = 'd'
637     unit_system = None
638     for (opt, val) in options:
639         if opt in '-D':
640             debug = int(val)
641         elif opt == '-l':
642             degreeformat = val
643         elif opt == '-u':
644             unit_system = val
645         elif opt in ('-?', '-h', '--help'):
646             print __doc__
647             sys.exit(0)
648         elif opt == 'V':
649             sys.stderr.write("xgps 1.0\n")
650             sys.exit(0)
651
652     degreefmt = {'d':gps.clienthelpers.deg_dd,
653                  'm':gps.clienthelpers.deg_ddmm,
654                  's':gps.clienthelpers.deg_ddmmss}[degreefmt]
655
656     (host, port, device) = ("localhost", "2947", None)
657     if len(arguments):
658         args = arguments[0].split(":")
659         if len(args) >= 1:
660             host = args[0]
661         if len(args) >= 2:
662             port = args[1]
663         if len(args) >= 3:
664             device = args[2]
665
666     base = Base(deg_type=degreefmt)
667     base.set_units(unit_system)
668     try:
669         daemon = gps.gps(host=host,
670                          port=port,
671                          mode=gps.WATCH_ENABLE|gps.WATCH_JSON|gps.WATCH_SCALED,
672                          verbose=debug)
673         base.watch(daemon, device)
674         base.main()
675     except socket.error:
676         w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
677                               flags=gtk.DIALOG_DESTROY_WITH_PARENT,
678                               buttons=gtk.BUTTONS_CANCEL)
679         w.set_markup("gpsd is not running.")
680         w.run()
681         w.destroy()
682