fixing routing.py bug, if time is 24:00
[pywienerlinien] / gotovienna-qml
1 #!/usr/bin/env python
2
3 """Public transport information for Vienna"""
4
5 __author__ = 'kelvan <kelvan@logic.at>'
6 __version__ = '0.9.1'
7 __website__ = 'http://tinyurl.com/gotoVienna'
8 __license__ = 'GNU General Public License v3 or later'
9
10 from datetime import datetime
11
12 from PySide.QtCore import QAbstractListModel, QModelIndex, QObject, Slot, Signal, Qt
13 from PySide.QtGui import QApplication, QTextItem
14 from PySide.QtDeclarative import QDeclarativeView
15
16 from gotovienna.utils import *
17 from gotovienna.realtime import *
18 from gotovienna.gps import *
19 from gotovienna.update import *
20 from gotovienna.config import config as conf
21
22 from gotovienna import defaults
23
24 import urllib2
25 import os
26 import sys
27 import threading
28 import json
29 from datetime import time
30
31 class Config(QObject):
32     def __init__(self):
33         QObject.__init__(self)
34
35     @Slot(result=bool)
36     def getGpsEnabled(self):
37         return conf.getGpsEnabled()
38
39     @Slot(bool, result=unicode)
40     def setGpsEnabled(self, val):
41         # TODO
42         return conf.setGpsEnabled(val)
43
44     @Slot(result=unicode)
45     def getLastUpdate(self):
46         # TODO
47         return conf.getLastStationsUpdate()
48
49     @Slot(result=unicode)
50     def updateStations(self):
51         # TODO
52         try:
53             update_stations()
54             return datetime.now().strftime('%c')
55         except Exception as e:
56             print e
57             return ''
58
59     @Slot(result=bool)
60     def checkStationsUpdate(self):
61         # FIXME exception handling foo
62         try:
63             return check_stations_update()
64         except:
65             return False
66
67 class AboutInfo(QObject):
68     def __init__(self):
69         QObject.__init__(self)
70
71     @Slot(result=unicode)
72     def getAppName(self):
73         return u'gotoVienna %s' % __version__
74
75     @Slot(result=unicode)
76     def getWebsiteURL(self):
77         return __website__
78
79     @Slot(result=unicode)
80     def getCopyright(self):
81         return 'Copyright 2011, 2012 %s' % __author__
82
83     @Slot(result=unicode)
84     def getLicense(self):
85         return __license__
86
87 class DepartureModel(QAbstractListModel):
88     LINE_ROLE = Qt.UserRole + 1
89     DIRECTION_ROLE = Qt.UserRole + 2
90     STATION_ROLE = Qt.UserRole + 3
91     TIME_ROLE = Qt.UserRole + 4
92     LOWFLOOR_ROLE = Qt.UserRole + 5
93
94     def __init__(self, parent=None):
95         super(DepartureModel, self).__init__(parent)
96         self._data = []
97         
98         self.keys = {}
99         self.keys[DepartureModel.LINE_ROLE] = 'line'
100         self.keys[DepartureModel.DIRECTION_ROLE] = 'direction'
101         self.keys[DepartureModel.STATION_ROLE] = 'station'
102         self.keys[DepartureModel.TIME_ROLE] = 'time'
103         self.keys[DepartureModel.LOWFLOOR_ROLE] = 'lowfloor'
104         self.setRoleNames(self.keys)
105
106     def rowCount(self, index):
107         return len(self._data)
108
109     def data(self, index, role):
110         if not index.isValid():
111             return None
112
113         if index.row() > len(self._data):
114             return None
115             
116         departure = self._data[index.row()]
117         
118         if self.keys.has_key(role):
119             return departure[self.keys[role]]
120         else:
121             return None
122             
123     def setDepartures(self, dep):
124         self.beginResetModel()
125         self._data = dep
126         self.endResetModel()
127
128 class FavoriteManager(QObject):
129     def __init__(self):
130         QObject.__init__(self)
131         self._faves = []
132         if os.path.exists(defaults.favorites_file):
133             try:
134                 self._faves = json.load(open(defaults.favorites_file, 'r'))
135                 self._faves = map(tuple, self._faves)
136                 print 'faves loaded:', self._faves
137             except Exception, e:
138                 print 'faves load error:', e
139
140     @Slot(result=int)
141     def getCount(self):
142         result = len(self._faves)
143         print 'getCount->', result
144         return result
145
146     @Slot(int, result=unicode)
147     def getItem(self, index):
148         keys = ['gline', 'gdirection', 'gstation', 'sourceurl', 'isstation']
149         result = dict(zip(keys, self._faves[index]))
150         result['name'] = u'%(gline)s -> %(gdirection)s @ %(gstation)s' % result
151         result = json.dumps(result)
152         print 'getItem:', index, result
153         return result
154
155     def _persist(self):
156         print 'persist:', self._faves, '->', defaults.favorites_file
157         try:
158             fp = open(defaults.favorites_file, 'w')
159             json.dump(self._faves, fp)
160             fp.close()
161         except Exception, e:
162             print 'faves save error:', e
163
164     @Slot(unicode, unicode, unicode, unicode, bool, int, result=bool)
165     def isFavorite(self, gline, gdirection, gstation, sourceurl, isstation, x):
166         k = (gline, gdirection, gstation, sourceurl, isstation)
167         return (k in self._faves)
168
169     @Slot(unicode, unicode, unicode, unicode, bool)
170     def toggleFavorite(self, gline, gdirection, gstation, sourceurl, isstation):
171         k = (gline, gdirection, gstation, sourceurl, isstation)
172         if k in self._faves:
173             self._faves.remove(k)
174         else:
175             self._faves.append(k)
176
177         self._persist()
178
179 class Gui(QObject):
180     def __init__(self, depModel):
181         QObject.__init__(self)
182         self.itip = ITipParser()
183         self.lines = []
184         self.departureModel = depModel
185
186         # Read line names in categorized/sorted order
187         for _, lines in categorize_lines(self.itip.lines):
188             self.lines.extend(lines)
189
190         self.current_line = ''
191         self.current_stations = []
192         self.current_departures = []
193
194     @Slot(int, result=str)
195     def get_direction(self, idx):
196         return self.current_stations[idx][0]
197
198     @Slot(str, str, result='QStringList')
199     def get_stations(self, line, direction):
200         print 'line:', line, 'current line:', self.current_line
201         for dx, stations in self.current_stations:
202             print 'dx:', dx, 'direction:', direction
203             if dx == direction:
204                 return [stationname for stationname, url in stations]
205
206         return ['no stations found']
207
208     directionsLoaded = Signal()
209
210     @Slot(str)
211     def load_directions(self, line):
212         def load_async():
213             stations = sorted(self.itip.get_stations(line).items())
214
215             self.current_line = line
216             self.current_stations = stations
217
218             self.directionsLoaded.emit()
219
220         threading.Thread(target=load_async).start()
221
222     #def map_departure(self, dep):
223     #    """ prepare departure list for qml gui
224     #    """
225     #    dep['lowfloor'] = 1 if dep['lowfloor'] else 0
226     #    dep['realtime'] = 1 if dep['realtime'] else 0
227     #    dep['time'] = dep['ftime']
228     #    return dep
229
230     departuresLoaded = Signal()
231
232     @Slot(str)
233     def load_departures_test(self, **args):
234         """ valid args combinations
235             station
236             line, station
237         """
238         def load_async():
239             if args.has_key('station'):
240                 if args.has_key('line'):
241                     self.current_departures = map(self.map_departure, \
242                                                   self.itip.get_departures(args['line'], args['station']))
243                     #print self.current_departures
244                     self.departuresLoaded.emit()
245                 else:
246                     self.current_departures = map(self.map_departure, \
247                                                   sort_departures(self.itip.get_departures_by_station(station)))
248             else:
249                 raise KeyError('Missing valid argument combination')
250
251         threading.Thread(target=load_async).start()
252
253     @Slot(str)
254     def load_departures(self, url):
255         def load_async():
256             self.departureModel.setDepartures(self.itip.get_departures(url))
257             #print self.current_departures
258             self.departuresLoaded.emit()
259
260         threading.Thread(target=load_async).start()
261
262     @Slot(str)
263     def load_station_departures(self, station):
264         def load_async():
265             self.departureModel.setDepartures(sort_departures(self.itip.get_departures_by_station(station)))
266             self.departuresLoaded.emit()
267
268         threading.Thread(target=load_async).start()
269
270     @Slot(float, float)
271     def load_nearby_departures(self, lat, lon):
272         def load_async():
273             self.current_departures = []
274             try:
275                 stations = get_nearby_stations(lat, lon)
276                 print stations
277                 for station in stations:
278                     print station
279                     try:
280                         self.current_departures += self.itip.get_departures_by_station(station)
281                     except Exception as e:
282                         print e.message
283                 self.current_departures = map(self.map_departure, \
284                                               sort_departures(self.current_departures))
285                 #print self.current_departures
286             except Exception as e:
287                 print e.message
288
289             print 'loaded'
290             self.departuresLoaded.emit()
291
292         threading.Thread(target=load_async).start()
293
294     @Slot(str, str, str, result=str)
295     def get_directions_url(self, line, direction, station):
296         return self.itip.get_url_from_direction(line, direction, station)
297
298     @Slot(result='QStringList')
299     def get_lines(self):
300         return self.lines
301
302     @Slot(float, float, result='QStringList')
303     def get_nearby_stations(self, lat, lon):
304         try:
305             return get_nearby_stations(lat, lon)
306         except BaseException as e:
307             # No/wrong stations.db file
308             return []
309
310
311 if __name__ == '__main__':
312     app = QApplication(sys.argv)
313
314     view = QDeclarativeView()
315
316     aboutInfo = AboutInfo()
317     config = Config()
318     departureModel = DepartureModel()
319
320     # instantiate the Python object
321     itip = Gui(departureModel)
322
323     favManager = FavoriteManager()
324
325     # expose the object to QML
326     context = view.rootContext()
327     context.setContextProperty('itip', itip)
328     context.setContextProperty('aboutInfo', aboutInfo)
329     context.setContextProperty('config', config)
330     context.setContextProperty('resultModel', departureModel)
331     context.setContextProperty('favManager', favManager)
332
333     if os.path.abspath(__file__).startswith('/usr/bin/'):
334         # Assume system-wide installation, QML from /usr/share/
335         view.setSource('/usr/share/gotovienna/qml/main.qml')
336     else:
337         # Assume test from source directory, use relative path
338         view.setSource(os.path.join(os.path.dirname(__file__), 'qml/main.qml'))
339
340     view.showFullScreen()
341
342     sys.exit(app.exec_())
343