add functions to menu buttons, move table code, change gps method to make more acturate
[gigfinder] / gig_finder.py
1 #!/usr/bin/env python2.5
2
3 """Simple program to display local gigs
4
5 Intended for use on the N900, uses the devices gps to find local gigs.
6 """
7
8 __authors__ = ["Jon Staley"]
9 __copyright__ = "Copyright 2010 Jon Staley"
10 __license__ = "MIT"
11 __version__ = "0.0.1"
12
13 from xml.dom.minidom import parseString
14 from datetime import datetime, date
15 import urllib
16 import time
17 import gtk
18 import hildon
19 import location
20 import time
21 import gobject
22 from threading import Thread
23 import thread
24
25 gtk.gdk.threads_init()
26
27 class GigParser:
28
29     def parse_xml(self, xml, lat, long):
30         """ Parse xml into a dict """
31         # TODO: filter to todays events only
32         events_list = []
33         today = date.today()
34         dom = parseString(xml)
35
36         events = dom.getElementsByTagName('event')
37         for event in events:
38             title = event.getElementsByTagName('title')[0].childNodes[0].data
39             
40             artists_element = event.getElementsByTagName('artists')[0]
41             artist_list = []
42             for artist in artists_element.getElementsByTagName('artist'):
43                 artist_list.append(artist.childNodes[0].data)
44             artists = ', '.join(artist_list)
45
46             venue_details = event.getElementsByTagName('venue')[0]
47             venue_name = venue_details.getElementsByTagName('name')[0].childNodes[0].data
48             address = self.get_address(venue_details.getElementsByTagName('location')[0])
49             geo_data = venue_details.getElementsByTagName('geo:point')[0]
50             venue_lat = geo_data.getElementsByTagName('geo:lat')[0].childNodes[0].data
51             venue_long = geo_data.getElementsByTagName('geo:long')[0].childNodes[0].data
52             distance = location.distance_between(float(lat), 
53                                                  float(long), 
54                                                  float(venue_lat), 
55                                                  float(venue_long))
56             
57             start_date = self.parse_date(event.getElementsByTagName('startDate')[0].childNodes[0].data)
58             
59             events_list.append({'title': title,
60                                 'venue': venue_name,
61                                 'address': address,
62                                 'distance': distance,
63                                 'artists': artists,
64                                 'date': start_date})
65         return events_list
66     
67     def get_address(self, location):
68         """ Return the venues address details from the xml element """
69         street = ''
70         city = ''
71         country = ''
72         postalcode = ''
73         if location.getElementsByTagName('street')[0].childNodes:
74             street = location.getElementsByTagName('street')[0].childNodes[0].data
75         if location.getElementsByTagName('city')[0].childNodes:
76             city = location.getElementsByTagName('city')[0].childNodes[0].data
77         if location.getElementsByTagName('country')[0].childNodes:
78             country = location.getElementsByTagName('country')[0].childNodes[0].data
79         if location.getElementsByTagName('postalcode')[0].childNodes:
80             postalcode = location.getElementsByTagName('postalcode')[0].childNodes[0].data
81         return '\n'.join([street, city, country, postalcode])
82
83     def parse_date(self, date_string):
84         """ Parse date string into datetime object """
85         fmt =  "%a, %d %b %Y %H:%M:%S"
86         result = time.strptime(date_string, fmt)
87         return datetime(result.tm_year, 
88                         result.tm_mon, 
89                         result.tm_mday, 
90                         result.tm_hour, 
91                         result.tm_min, 
92                         result.tm_sec)
93
94 class LocationUpdater:
95
96     def __init__(self):
97         self.lat = None
98         self.long = None
99         self.loop = gobject.MainLoop()
100
101         self.control = location.GPSDControl.get_default()
102         self.control.set_properties(preferred_method=location.METHOD_GNSS,
103                                preferred_interval=location.INTERVAL_DEFAULT)
104         self.control.connect("error-verbose", self.on_error, self.loop)
105         self.control.connect("gpsd-stopped", self.on_stop, self.loop)
106
107         self.device = location.GPSDevice()
108         self.device.connect("changed", self.on_changed, self.control)
109
110     def update_location(self):
111         """ Run the loop and update lat and long """
112         self.reset()
113         gobject.idle_add(self.start_location, self.control)
114         self.loop.run()
115
116     def on_error(self, control, error, data):
117         """ Handle errors """
118         print "location error: %d... quitting" % error
119         data.quit()
120
121     def on_changed(self, device, data):
122         """ Set long and lat """
123         if not device:
124             return
125         if device.fix:
126             # once fix is found and long, lat available set long lat
127             if device.fix[1] & location.GPS_DEVICE_LATLONG_SET:
128                 self.lat, self.long = device.fix[4:6]
129                 data.stop()
130
131     def on_stop(self, control, data):
132         """ Stop the location service """
133         print "quitting"
134         data.quit()
135
136     def start_location(self, data):
137         """ Start the location service """
138         data.start()
139         return False
140
141     def reset(self):
142         """ Reset coordinates """
143         self.lat = None
144         self.long = None
145         self.device.reset_last_known()
146
147 class GigFinder:
148
149     def __init__(self):
150         self.lat = None
151         self.long = None
152         self.url_base = "http://ws.audioscrobbler.com/2.0/"
153         self.api_key = "1928a14bdf51369505530949d8b7e1ee"
154         self.distance = '10'
155         self.banner = None
156         self.parser = GigParser()
157         self.location = LocationUpdater()
158         self.win = hildon.StackableWindow()
159         self.app_title = "Gig Finder"
160         # TODO: Add preferences for distance, refactor gui code, maybe do km
161         # to mile conversions
162
163     def main(self):
164         """ Build the gui and start the update thread """
165         program = hildon.Program.get_instance()
166         menu = self.create_menu()
167
168         self.win.set_title(self.app_title)
169         self.win.connect("destroy", gtk.main_quit, None)
170         self.win.set_app_menu(menu)
171
172         Thread(target=self.update_gigs).start()
173
174         self.win.show_all()
175         gtk.main()
176
177     def show_about(self, widget, data):
178         """ Show about dialog """
179         dialog = gtk.AboutDialog()
180         dialog.set_name('Gig Finder')
181         dialog.set_version(__version__)
182         dialog.set_authors(__authors__)
183         dialog.set_comments('Display gigs close by.\nUsing the http://www.last.fm api.')
184         dialog.set_license('Distributed under the MIT license.\nhttp://www.opensource.org/licenses/mit-license.php')
185         dialog.set_copyright(__copyright__)
186         dialog.show_all()
187
188     def update(self, widget, data):
189         """ Start update process """
190         self.win.set_title(self.app_title)
191         self.location.reset()
192         self.win.remove(self.pannable_area)
193         Thread(target=self.update_gigs).start()
194
195     def update_gigs(self):
196         """ Get gig info """
197         gobject.idle_add(self.show_message, "Getting events")
198         gobject.idle_add(self.location.update_location)
199
200         # if no gps fix wait
201         while not self.location.lat or not self.location.long:
202             time.sleep(1)
203
204         events = self.get_events(self.location.lat, self.location.long)
205         gobject.idle_add(self.hide_message)
206         gobject.idle_add(self.show_events, events)
207         thread.exit()
208
209     def show_message(self, message):
210         """ Set window progress indicator and show message """
211         hildon.hildon_gtk_window_set_progress_indicator(self.win, 1)
212         self.banner = hildon.hildon_banner_show_information(self.win,
213                                                             '', 
214                                                             message)
215
216     def hide_message(self):
217         """ Hide banner and sete progress indicator """
218         self.banner.hide()
219         hildon.hildon_gtk_window_set_progress_indicator(self.win, 0)
220
221     def get_events(self, lat, long):
222         """ Retrieve xml and parse into events list """
223         xml = self.get_xml(lat, long)
224         events = self.parser.parse_xml(xml, 
225                                        lat,
226                                        long)
227         return events
228
229     def show_events(self, events):
230         """ Sort events, set new window title and add events to table """
231         if events:
232             events = self.sort_gigs(events)
233             self.win.set_title('%s (%s)' % (self.app_title, len(events)))
234             self.add_events(events)
235         else:
236             label = gtk.Label('No events available')
237             vbox = gtk.VBox(False, 0)
238             vbox.pack_start(label, True, True, 0)
239             vbox.show_all()
240             self.win.add(vbox)
241
242     def distance_cmp(self, x, y):
243         """ Compare distances for list sort """
244         if x > y:
245             return 1
246         elif x == y:
247             return 0
248         else:
249             return -1
250
251     def sort_gigs(self, events):
252         """ Sort gig by distance """
253         events.sort(cmp=self.distance_cmp, key=lambda x: x['distance'])
254         return events
255         
256     def get_xml(self, lat, long):
257         """ Return xml from lastfm """
258         method = "geo.getevents"
259         params = urllib.urlencode({'method': method,
260                                    'api_key': self.api_key,
261                                    'distance': self.distance,
262                                    'long': long,
263                                    'lat': lat})
264         response = urllib.urlopen(self.url_base, params)
265         return response.read()
266
267     def create_menu(self):
268         """ Build application menu """
269         update_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
270         update_button.set_label('Update')
271         update_button.connect('clicked',
272                               self.update,
273                               None)
274
275         about_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
276         about_button.set_label('About')
277         about_button.connect('clicked',
278                              self.show_about,
279                              None)
280
281         menu = hildon.AppMenu()
282         menu.append(update_button)
283         menu.append(about_button)
284         menu.show_all()
285         return menu
286
287     def show_details(self, widget, data):
288         """ Open new window showing gig details """
289         win = hildon.StackableWindow()
290         win.set_title(data['title'])
291
292         win.vbox = gtk.VBox()
293         win.add(win.vbox)
294
295         scroll = hildon.PannableArea()
296         win.vbox.pack_start(scroll, True, True, 0)
297
298         view = hildon.TextView()
299         view.set_editable(False)
300         view.unset_flags(gtk.CAN_FOCUS)
301         view.set_wrap_mode(gtk.WRAP_WORD)
302         buffer = view.get_buffer()
303         end = buffer.get_end_iter()
304         buffer.insert(end, '%s\n' % data['title'])
305         buffer.insert(end, 'Artists: %s\n' % data['artists'])
306         buffer.insert(end, 'Venue: %s\n' % data['venue'])
307         buffer.insert(end, '%s\n' % data['address'])
308         buffer.insert(end, 'When: %s\n' % data['date'].strftime('%H:%M'))
309         buffer.insert(end, '\n')
310         scroll.add_with_viewport(view)
311
312         win.show_all()
313
314     def add_table(self):
315         """ Add table for events """
316         self.table = gtk.Table(columns=1)
317         self.table.set_row_spacings(10)
318         self.table.set_col_spacings(10)
319
320         self.pannable_area = hildon.PannableArea()
321         self.pannable_area.add_with_viewport(self.table)
322         self.pannable_area.show_all()
323         self.win.add(self.pannable_area)
324         
325     def add_events(self, events):
326         """ Add a table of buttons """
327         self.add_table()
328         pos = 0
329         for event in events:
330             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, 
331                                    hildon.BUTTON_ARRANGEMENT_VERTICAL)
332             button.set_text(event['title'], "distance: %0.02f km" % event['distance'])
333             button.connect("clicked", self.show_details, event)
334             self.table.attach(button, 0, 1, pos, pos+1)
335             pos += 1
336         self.table.show_all()
337    
338 if __name__ == "__main__":
339     finder = GigFinder()
340     finder.main()