Favorites draft implementation
[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.0'
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
13 from PySide.QtGui import QApplication
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 FavoriteManager(QObject):
88     def __init__(self):
89         QObject.__init__(self)
90         self._faves = []
91         if os.path.exists(defaults.favorites_file):
92             try:
93                 self._faves = json.load(open(defaults.favorites_file, 'r'))
94                 self._faves = map(tuple, self._faves)
95                 print 'faves loaded:', self._faves
96             except Exception, e:
97                 print 'faves load error:', e
98
99     @Slot(result=int)
100     def getCount(self):
101         result = len(self._faves)
102         print 'getCount->', result
103         return result
104
105     @Slot(int, result=unicode)
106     def getItem(self, index):
107         keys = ['gline', 'gdirection', 'gstation', 'sourceurl', 'isstation']
108         result = dict(zip(keys, self._faves[index]))
109         result['name'] = u'%(gline)s -> %(gdirection)s @ %(gstation)s' % result
110         result = json.dumps(result)
111         print 'getItem:', index, result
112         return result
113
114     def _persist(self):
115         print 'persist:', self._faves, '->', defaults.favorites_file
116         try:
117             fp = open(defaults.favorites_file, 'w')
118             json.dump(self._faves, fp)
119             fp.close()
120         except Exception, e:
121             print 'faves save error:', e
122
123     @Slot(unicode, unicode, unicode, unicode, bool, int, result=bool)
124     def isFavorite(self, gline, gdirection, gstation, sourceurl, isstation, x):
125         k = (gline, gdirection, gstation, sourceurl, isstation)
126         return (k in self._faves)
127
128     @Slot(unicode, unicode, unicode, unicode, bool)
129     def toggleFavorite(self, gline, gdirection, gstation, sourceurl, isstation):
130         k = (gline, gdirection, gstation, sourceurl, isstation)
131         if k in self._faves:
132             self._faves.remove(k)
133         else:
134             self._faves.append(k)
135
136         self._persist()
137
138
139 class GotoViennaListModel(QAbstractListModel):
140     def __init__(self, objects=None):
141         QAbstractListModel.__init__(self)
142         if objects is None:
143             objects = []
144         self._objects = objects
145         self.setRoleNames({0: 'modelData'})
146
147     def set_objects(self, objects):
148         self._objects = objects
149         self.reset()
150
151     def get_objects(self):
152         return self._objects
153
154     def get_object(self, index):
155         return self._objects[index.row()]
156
157     def rowCount(self, parent=QModelIndex()):
158         return len(self._objects)
159
160     def data(self, index, role):
161         if index.isValid():
162             if role == 0:
163                 return self.get_object(index)
164         return None
165
166
167 class Gui(QObject):
168     def __init__(self):
169         QObject.__init__(self)
170         self.itip = ITipParser()
171         self.lines = []
172
173         # Read line names in categorized/sorted order
174         for _, lines in categorize_lines(self.itip.lines):
175             self.lines.extend(lines)
176
177         self.current_line = ''
178         self.current_stations = []
179         self.current_departures = []
180
181     @Slot(int, result=str)
182     def get_direction(self, idx):
183         return self.current_stations[idx][0]
184
185     @Slot(str, str, result='QStringList')
186     def get_stations(self, line, direction):
187         print 'line:', line, 'current line:', self.current_line
188         for dx, stations in self.current_stations:
189             print 'dx:', dx, 'direction:', direction
190             if dx == direction:
191                 return [stationname for stationname, url in stations]
192
193         return ['no stations found']
194
195     directionsLoaded = Signal()
196
197     @Slot(str)
198     def load_directions(self, line):
199         def load_async():
200             stations = sorted(self.itip.get_stations(line).items())
201
202             self.current_line = line
203             self.current_stations = stations
204
205             self.directionsLoaded.emit()
206
207         threading.Thread(target=load_async).start()
208
209     def map_departure(self, dep):
210         """ prepare departure list for qml gui
211         """
212         dep['lowfloor'] = 1 if dep['lowfloor'] else 0
213         dep['realtime'] = 1 if dep['realtime'] else 0
214         dep['time'] = dep['ftime']
215         return dep
216
217     departuresLoaded = Signal()
218
219     @Slot(str)
220     def load_departures_test(self, **args):
221         """ valid args combinations
222             station
223             line, station
224         """
225         def load_async():
226             if args.has_key('station'):
227                 if args.has_key('line'):
228                     self.current_departures = map(self.map_departure, \
229                                                   self.itip.get_departures(args['line'], args['station']))
230                     #print self.current_departures
231                     self.departuresLoaded.emit()
232                 else:
233                     self.current_departures = map(self.map_departure, \
234                                                   sort_departures(self.itip.get_departures_by_station(station)))
235             else:
236                 raise KeyError('Missing valid argument combination')
237
238         threading.Thread(target=load_async).start()
239
240     @Slot(str)
241     def load_departures(self, url):
242         def load_async():
243             self.current_departures = map(self.map_departure, \
244                                           self.itip.get_departures(url))
245             #print self.current_departures
246             self.departuresLoaded.emit()
247
248         threading.Thread(target=load_async).start()
249
250     @Slot(str)
251     def load_station_departures(self, station):
252         def load_async():
253             self.current_departures = map(self.map_departure, \
254                                           sort_departures(self.itip.get_departures_by_station(station)))
255             #print self.current_departures
256             self.departuresLoaded.emit()
257
258         threading.Thread(target=load_async).start()
259
260     @Slot(float, float)
261     def load_nearby_departures(self, lat, lon):
262         def load_async():
263             self.current_departures = []
264             try:
265                 stations = get_nearby_stations(lat, lon)
266                 print stations
267                 for station in stations:
268                     print station
269                     try:
270                         self.current_departures += self.itip.get_departures_by_station(station)
271                     except Exception as e:
272                         print e.message
273                 self.current_departures = map(self.map_departure, \
274                                               sort_departures(self.current_departures))
275                 #print self.current_departures
276             except Exception as e:
277                 print e.message
278
279             print 'loaded'
280             self.departuresLoaded.emit()
281
282         threading.Thread(target=load_async).start()
283
284     @Slot(str, str, str, result=str)
285     def get_directions_url(self, line, direction, station):
286         return self.itip.get_url_from_direction(line, direction, station)
287
288     @Slot(result='QStringList')
289     def get_lines(self):
290         return self.lines
291
292     @Slot(result='QVariant')
293     def get_departures(self):
294         return self.current_departures
295
296     @Slot(float, float, result='QStringList')
297     def get_nearby_stations(self, lat, lon):
298         try:
299             return get_nearby_stations(lat, lon)
300         except BaseException as e:
301             # No/wrong stations.db file
302             return []
303
304     @Slot(str, str)
305     def search(self, line, station):
306         line = line.upper()
307         station = station.decode('utf-8')
308         print line, station
309
310         if line not in self.lines:
311             return "Invalid line"
312
313         try:
314             stations = sorted(self.itip.get_stations(line).items())
315             print stations
316             headers, stations = zip(*stations)
317             print headers
318             print stations
319             details = [(direction, name, url) for direction, stops in stations
320                         for name, url in stops if match_station(station, name)]
321             print details
322         except urllib2.URLError as e:
323             print e.message
324             return e.message
325
326 if __name__ == '__main__':
327     app = QApplication(sys.argv)
328
329     view = QDeclarativeView()
330
331     aboutInfo = AboutInfo()
332     config = Config()
333
334     # instantiate the Python object
335     itip = Gui()
336
337     favManager = FavoriteManager()
338
339     # expose the object to QML
340     context = view.rootContext()
341     context.setContextProperty('itip', itip)
342     context.setContextProperty('aboutInfo', aboutInfo)
343     context.setContextProperty('config', config)
344     context.setContextProperty('favManager', favManager)
345
346     if os.path.abspath(__file__).startswith('/usr/bin/'):
347         # Assume system-wide installation, QML from /usr/share/
348         view.setSource('/usr/share/gotovienna/qml/main.qml')
349     else:
350         # Assume test from source directory, use relative path
351         view.setSource(os.path.join(os.path.dirname(__file__), 'qml/main.qml'))
352
353     view.showFullScreen()
354
355     sys.exit(app.exec_())
356