source: subversion/applications/routing/pyroute/gui.py @ 20924

Last change on this file since 20924 was 12252, checked in by jeansch, 11 years ago

Fixed 'tah' URL
Fixed starting from another directory
Implemented 'scroll' events to allow to change zoom from mouse-wheel
Implemented initial position

  • Property svn:executable set to *
File size: 12.7 KB
Line 
1#!/usr/bin/python
2#-----------------------------------------------------------------------------
3# Pyroute main GUI
4#
5# Usage:
6#   gui.py
7#
8# Controls:
9#   * drag left/right along top of window to zoom in/out
10#   * drag the map to move around
11#   * click on the map for a position menu
12#       * set your own position, if the GPS didn't already do that
13#       * set that location as a destination
14#       * route to that point
15#   * click on the top-left of the window for the main menu
16#       * download routing data around your position
17#       * browse geoRSS, wikipedia, and OSM points of interest
18#       * select your mode of transport for routing
19#           * car, bike, foot currently supported
20#       * toggle whether the map is centred on your GPS position
21#-----------------------------------------------------------------------------
22# Copyright 2007, Oliver White
23#
24# This program is free software: you can redistribute it and/or modify
25# it under the terms of the GNU General Public License as published by
26# the Free Software Foundation, either version 3 of the License, or
27# (at your option) any later version.
28#
29# This program is distributed in the hope that it will be useful,
30# but WITHOUT ANY WARRANTY; without even the implied warranty of
31# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
32# GNU General Public License for more details.
33#
34# You should have received a copy of the GNU General Public License
35# along with this program.  If not, see <http://www.gnu.org/licenses/>.
36#-----------------------------------------------------------------------------
37import pygtk
38pygtk.require('2.0')
39import gobject
40import gtk
41import sys
42import cairo
43import urllib
44import os
45from math import sqrt
46from time import clock
47from gtk import gdk
48
49# Our modules:
50from mod_osmData import osmData
51from routeOrDirect import *
52from tilenames import *
53from geoPosition import *
54from projection import Projection
55from overlay import *
56from dataStore import *
57from poi_geoRss import geoRss
58from poi_geonames import geonames
59from poi_waypoints import waypointsModule
60from poi_osm import osmPoiModule
61from base import pyrouteModule
62from tiles import tileHandler
63from events import pyrouteEvents
64from sketch import sketching
65from tracklog import tracklog
66from meta import moduleInfo
67
68def update(mapWidget):
69  mapWidget.update();
70  return(True)
71
72class MapWidget(gtk.Widget, pyrouteModule):
73  __gsignals__ = { \
74    'realize': 'override',
75    'expose-event' : 'override',
76    'size-allocate': 'override',
77    'size-request': 'override'
78    }
79  def __init__(self):
80    gtk.Widget.__init__(self)
81    self.draw_gc = None
82   
83    self.modules = {'poi':{}}
84    pyrouteModule.__init__(self, self.modules)
85   
86    self.loadModules()
87    self.blankData()
88    self.loadData()
89   
90    self.updatePosition()
91   
92    self.timer = gobject.timeout_add(100, update, self)
93   
94  def loadModules(self):
95    self.modules['poi']['rss'] = geoRss(self.modules,
96                                        os.path.join(os.path.dirname(__file__),
97                                                     'Setup', 'feeds.txt'))
98    #self.modules['poi']['geonames'] = geonames(self.modules)
99    #self.modules['poi']['waypoints'] = waypointsModule(self.modules, "data/waypoints.gpx")
100    self.modules['poi']['osm'] = osmPoiModule(self.modules)
101    self.modules['overlay'] = guiOverlay(self.modules)
102    self.modules['position'] = geoPosition()
103    self.modules['tiles'] = tileHandler(self.modules)
104    self.modules['data'] = DataStore(self.modules)
105    self.modules['events'] = pyrouteEvents(self.modules)
106    self.modules['sketch'] = sketching(self.modules)
107    self.modules['osmdata'] = osmData(self.modules)
108    self.modules['projection'] = Projection()
109    self.modules['tracklog'] = tracklog(self.modules)
110    self.modules['meta'] = moduleInfo(self.modules)
111    self.modules['route'] = RouteOrDirect(self.modules['osmdata'].data)
112   
113  def blankData(self):
114    self.set('ownpos', {'valid':False})
115    self.set('mode','cycle')
116    self.set('centred',False)
117    self.set('logging',True)
118    self.m['projection'].recentre(51.3,-0.1, 9)
119 
120  def loadData(self):
121    self.m['sketch'].load("data/sketches/latest.gpx")
122    self.m['tracklog'].load("data/track.gpx")
123    self.m['osmdata'].loadDefault()
124   
125  def beforeDie(self):
126    print "Handling last few checks before we close"
127    self.m['tracklog'].checkIfNeedsSaving(True)
128 
129  def update(self):
130    self.updatePosition()
131    if(self.get("needRedraw")):
132      self.forceRedraw()
133     
134  def updatePosition(self):
135    """Try to get our position from GPS"""
136    newpos = self.modules['position'].get()
137   
138    # If the latest position wasn't valid, then don't touch the old copy
139    # (especially since we might have manually set the position)
140    if(not newpos['valid']):
141      return
142   
143    #check if new pos is different than current
144    oldpos = self.get('ownpos');
145    if ((oldpos['valid']) and (oldpos['lon'] == newpos['lon']) and (oldpos['lat'] == oldpos['lat'])):
146        return
147
148    # TODO: if we set ownpos and then get a GPS signal, we should decide
149    # here what to do
150    self.set('ownpos', newpos)
151   
152    self.handleUpdatedPosition()
153   
154  def handleUpdatedPosition(self):
155    pos = self.get('ownpos')
156    if(not pos['valid']):
157      return
158 
159    # Add latest position to tracklog 
160    if(self.get('logging')):
161      self.m['tracklog'].addPoint(pos['lat'], pos['lon'])
162      self.m['tracklog'].checkIfNeedsSaving()
163   
164    # If we've never actually decided where to display a map yet, do so now
165    if(not self.modules['projection'].isValid()):
166      print "Projection not yet valid, centering on ownpos"
167      self.centreOnOwnPos()
168      return
169   
170    # This code would let us recentre if we reach the edge of the screen -
171    # it's not currently in use
172    x,y = self.modules['projection'].ll2xy(pos['lat'], pos['lon'])
173    x,y = self.modules['projection'].relXY(x,y)
174    border = 0.15
175    outsideMap = (x < border or y < border or x > (1-border) or y > (1-border))
176   
177    # If map is locked to our position, then recentre it
178    if(self.get('centred')):
179      self.centreOnOwnPos()
180    else:
181      self.forceRedraw()
182 
183  def centreOnOwnPos(self):
184    """Try to centre the map on our position"""
185    pos = self.get('ownpos')
186    if(pos['valid']):
187      self.modules['projection'].recentre(pos['lat'], pos['lon'])
188      self.forceRedraw()
189
190  def mousedown(self,x,y):
191    if(self.get('sketch',0)):
192      self.m['sketch'].startStroke(x,y)
193   
194  def click(self, x, y):
195    print "Clicked %d,%d"%(x,y)
196    """Handle clicking on the screen"""
197    # Give the overlay a chance to handle all clicks first
198    if(self.m['overlay'].handleClick(x,y)):
199      pass
200    # If the overlay is fullscreen and it didn't respond, the click does
201    # not fall-through to the map
202    elif(self.m['overlay'].fullscreen()):
203      return
204    elif(self.get('sketch',0)):
205      return
206    # Map was clicked-on: store the lat/lon and go into the "clicked" menu
207    else:
208      lat, lon = self.m['projection'].xy2ll(x,y)
209      self.set('clicked', (lat,lon))
210      self.set('menu','click')
211    self.forceRedraw()
212     
213  def forceRedraw(self):
214    """Make the window trigger a draw event. 
215    TODO: consider replacing this if porting pyroute to another platform"""
216    self.set("needRedraw", False)
217    try:
218      self.window.invalidate_rect((0,0,self.rect.width,self.rect.height),False)
219    except AttributeError:
220      pass
221   
222  def move(self,dx,dy):
223    """Handle dragging the map"""
224   
225    # TODO: what happens when you drag inside menus?
226    if(self.get('menu')):
227      return
228    if(self.get('centred') and self.get('ownpos')['valid']):
229      return
230    self.modules['projection'].nudge(dx,dy)
231    self.forceRedraw()
232 
233  def zoom(self,dx):
234    """Handle dragging left/right along top of the screen to zoom"""
235    self.modules['projection'].nudgeZoom(-1 * dx / self.rect.width)
236    self.forceRedraw()
237
238  def handleDrag(self,x,y,dx,dy,startX,startY):
239    if(self.modules['overlay'].fullscreen()):
240      if(self.modules['overlay'].handleDrag(dx,dy,startX,startY)):
241        self.forceRedraw()
242    elif(self.get('sketch',0)):
243      self.m['sketch'].moveTo(x,y)
244      return
245    else:
246      self.move(dx,dy)
247   
248
249  def draw(self, cr):
250    start = clock()
251    proj = self.modules['projection']
252   
253    # Don't draw the map if a menu/overlay is fullscreen
254    if(not self.modules['overlay'].fullscreen()):
255     
256      # Map as image
257      self.modules['tiles'].draw(cr)
258
259      # The route
260      if(self.modules['route'].valid()):
261        cr.set_source_rgba(0.5, 0.0, 0.0, 0.5)
262        cr.set_line_width(12)
263        count = 0
264        for i in self.modules['route'].route['route']:
265          x,y = proj.ll2xy(i[0],i[1])
266          if(count == 0):
267            cr.move_to(x,y)
268          else:
269            cr.line_to(x,y)
270          count = count + 1
271        cr.stroke()
272     
273      # Each plugin can display on the map
274      for name,source in self.modules['poi'].items():
275        if source.draw:
276          for group in source.groups:
277            for item in group.items:
278              x,y = proj.ll2xy(item.lat, item.lon)
279              if(proj.onscreen(x,y)):
280                cr.set_source_rgb(0.0, 0.4, 0.0)
281                cr.set_font_size(12)
282                cr.move_to(x,y)
283                cr.show_text(item.title)
284                cr.stroke()
285
286      self.m['sketch'].draw(cr,proj)
287      self.m['tracklog'].draw(cr,proj)
288     
289      pos = self.get('ownpos')
290      if(pos['valid']):
291        # Us
292        x,y = proj.ll2xy(pos['lat'], pos['lon'])
293        cr.set_source_rgb(0.0, 0.0, 0.0)
294        cr.arc(x,y,14, 0,2*3.1415)
295        cr.fill()
296        cr.set_source_rgb(1.0, 0.0, 0.0)
297        cr.arc(x,y,10, 0,2*3.1415)
298        cr.fill()
299     
300
301    # Overlay (menus etc)
302    self.modules['overlay'].draw(cr, self.rect)
303   
304    #print "Map took %1.2f ms" % (1000 * (clock() - start))
305   
306  def do_realize(self):
307    self.set_flags(self.flags() | gtk.REALIZED)
308    self.window = gdk.Window( \
309      self.get_parent_window(),
310      width = self.allocation.width,
311      height = self.allocation.height,
312      window_type = gdk.WINDOW_CHILD,
313      wclass = gdk.INPUT_OUTPUT,
314      event_mask = self.get_events() | gdk.EXPOSURE_MASK)
315    self.window.set_user_data(self)
316    self.style.attach(self.window)
317    self.style.set_background(self.window, gtk.STATE_NORMAL)
318    self.window.move_resize(*self.allocation)
319  def do_size_request(self, allocation):
320    pass
321  def do_size_allocate(self, allocation):
322    self.allocation = allocation
323    if self.flags() & gtk.REALIZED:
324      self.window.move_resize(*allocation)
325  def _expose_cairo(self, event, cr):
326    self.rect = self.allocation
327    self.modules['projection'].setView( \
328      self.rect.x, 
329      self.rect.y, 
330      self.rect.width, 
331      self.rect.height)
332    self.draw(cr)
333  def do_expose_event(self, event):
334    self.chain(event)
335    cr = self.window.cairo_create()
336    return self._expose_cairo(event, cr)
337
338class GuiBase:
339  """Wrapper class for a GUI interface"""
340  def __init__(self):
341    # Create the window
342    win = gtk.Window()
343    win.set_title('pyroute')
344    win.connect('delete-event', gtk.main_quit)
345    win.resize(480,640)
346    win.move(50, gtk.gdk.screen_height() - 650)
347   
348    # Events
349    event_box = gtk.EventBox()
350    event_box.connect("button_press_event", lambda w,e: self.pressed(e))
351    event_box.connect("button_release_event", lambda w,e: self.released(e))
352    event_box.connect("scroll-event", lambda w,e: self.scrolled(e))
353    event_box.connect("motion_notify_event", lambda w,e: self.moved(e))
354    win.add(event_box)
355   
356    # Create the map
357    self.mapWidget = MapWidget()
358    event_box.add(self.mapWidget)
359   
360    # Finalise the window
361    win.show_all()
362    gtk.main()
363    self.mapWidget.beforeDie()
364   
365  def pressed(self, event):
366    self.dragstartx = event.x
367    self.dragstarty = event.y
368    #print dir(event)
369    #print "Pressed button %d at %1.0f, %1.0f" % (event.button, event.x, event.y)
370   
371    self.dragx = event.x
372    self.dragy = event.y
373    self.mapWidget.mousedown(event.x,event.y)
374
375  def scrolled(self, event):
376    if event.direction == gtk.gdk.SCROLL_UP:
377      self.mapWidget.modules['projection'].setZoom(1, True)
378      self.mapWidget.set("needRedraw", True)
379     
380    if event.direction == gtk.gdk.SCROLL_DOWN:
381      self.mapWidget.modules['projection'].setZoom(-1, True)
382      self.mapWidget.set("needRedraw", True)
383     
384  def moved(self, event):
385    """Drag-handler"""
386
387    self.mapWidget.handleDrag( \
388      event.x,
389      event.y,
390      event.x - self.dragx, 
391      event.y - self.dragy,
392      self.dragstartx,
393      self.dragstarty)
394
395    self.dragx = event.x
396    self.dragy = event.y
397  def released(self, event):
398    dx = event.x - self.dragstartx
399    dy = event.y - self.dragstarty
400    distSq = dx * dx + dy * dy
401    if distSq < 4:
402      self.mapWidget.click(event.x, event.y)
403
404
405if __name__ == "__main__":
406  program = GuiBase()
Note: See TracBrowser for help on using the repository browser.