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

Last change on this file since 5853 was 5850, checked in by ojw, 12 years ago

convert to an "integer zoom level" (so zooming isn't continuous, but must double or halve the detail in each step) -- this is to make the map images the same size as their original, so that cairo doesn't have to do any scaling

  • Property svn:executable set to *
File size: 11.3 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 loadOsm import *
51from routeOrDirect import *
52from tilenames import *
53from geoPosition import *
54from projection import Projection
55from overlay import *
56from dataStore import *
57from mod_geoRss import geoRss
58from mod_geonames import geonames
59from mod_waypoints import waypointsModule
60from base import pyrouteModule
61from tiles import tileHandler
62from events import pyrouteEvents
63from sketch import sketching
64from tracklog import tracklog
65
66def update(mapWidget):
67  mapWidget.update();
68  return(True)
69
70class MapWidget(gtk.Widget, pyrouteModule):
71  __gsignals__ = { \
72    'realize': 'override',
73    'expose-event' : 'override',
74    'size-allocate': 'override',
75    'size-request': 'override'
76    }
77  def __init__(self):
78    gtk.Widget.__init__(self)
79    self.draw_gc = None
80    self.timer = gobject.timeout_add(100, update, self)
81   
82    self.modules = {'poi':{}}
83    pyrouteModule.__init__(self, self.modules)
84   
85    #self.modules['poi']['rss'] = geoRss('Setup/feeds.txt')
86    #self.modules['poi']['geonames'] = geonames()
87    self.modules['poi']['waypoints'] = waypointsModule(self.modules, "data/waypoints.gpx")
88    self.modules['overlay'] = guiOverlay(self.modules)
89    self.modules['position'] = geoPosition()
90    self.modules['tiles'] = tileHandler(self.modules)
91    self.modules['data'] = DataStore(self.modules)
92    self.modules['events'] = pyrouteEvents(self.modules)
93    self.modules['sketch'] = sketching(self.modules)
94    self.modules['sketch'].load("data/sketches/latest.gpx")
95    self.set('mode','cycle')
96    self.set('centred',False)
97    self.set('logging',True)
98    self.modules['osmdata'] = LoadOsm(None)
99    self.modules['projection'] = Projection()
100    self.modules['projection'].recentre(51.3,-0.1, 9)
101    self.modules['tracklog'] = tracklog(self.modules)
102    # self.modules['tracklog'].load("data/track.gpx")
103    self.modules['route'] = RouteOrDirect(self.modules['osmdata'])
104    self.updatePosition()
105    self.set('ownpos', {'valid':False})
106
107  def update(self):
108    self.updatePosition()
109    if(self.get("needRedraw")):
110      self.forceRedraw()
111     
112  def updatePosition(self):
113    """Try to get our position from GPS"""
114    newpos = self.modules['position'].get()
115   
116    # If the latest position wasn't valid, then don't touch the old copy
117    # (especially since we might have manually set the position)
118    if(not newpos['valid']):
119      return
120   
121    # TODO: if we set ownpos and then get a GPS signal, we should decide
122    # here what to do
123    self.set('ownpos', newpos)
124   
125    self.handleUpdatedPosition()
126   
127  def handleUpdatedPosition(self):
128    pos = self.get('ownpos')
129    if(not pos['valid']):
130      return
131 
132    # Add latest position to tracklog 
133    if(self.get('logging')):
134      self.m['tracklog'].addPoint(pos['lat'], pos['lon'])
135   
136    # If we've never actually decided where to display a map yet, do so now
137    if(not self.modules['projection'].isValid()):
138      print "Projection not yet valid, centering on ownpos"
139      self.centreOnOwnPos()
140      return
141   
142    # This code would let us recentre if we reach the edge of the screen -
143    # it's not currently in use
144    x,y = self.modules['projection'].ll2xy(pos['lat'], pos['lon'])
145    x,y = self.modules['projection'].relXY(x,y)
146    border = 0.15
147    outsideMap = (x < border or y < border or x > (1-border) or y > (1-border))
148   
149    # If map is locked to our position, then recentre it
150    if(self.get('centred')):
151      self.centreOnOwnPos()
152    else:
153      self.forceRedraw()
154 
155  def centreOnOwnPos(self):
156    """Try to centre the map on our position"""
157    pos = self.get('ownpos')
158    if(pos['valid']):
159      self.modules['projection'].recentre(pos['lat'], pos['lon'])
160      self.forceRedraw()
161
162  def mousedown(self,x,y):
163    if(self.get('sketch',0)):
164      self.m['sketch'].startStroke(x,y)
165   
166  def click(self, x, y):
167    print "Clicked %d,%d"%(x,y)
168    """Handle clicking on the screen"""
169    # Give the overlay a chance to handle all clicks first
170    if(self.m['overlay'].handleClick(x,y)):
171      pass
172    # If the overlay is fullscreen and it didn't respond, the click does
173    # not fall-through to the map
174    elif(self.m['overlay'].fullscreen()):
175      return
176    elif(self.get('sketch',0)):
177      return
178    # Map was clicked-on: store the lat/lon and go into the "clicked" menu
179    else:
180      lat, lon = self.m['projection'].xy2ll(x,y)
181      self.set('clicked', (lat,lon))
182      self.set('menu','click')
183    self.forceRedraw()
184     
185  def forceRedraw(self):
186    """Make the window trigger a draw event. 
187    TODO: consider replacing this if porting pyroute to another platform"""
188    self.set("needRedraw", False)
189    try:
190      self.window.invalidate_rect((0,0,self.rect.width,self.rect.height),False)
191    except AttributeError:
192      pass
193   
194  def move(self,dx,dy):
195    """Handle dragging the map"""
196   
197    # TODO: what happens when you drag inside menus?
198    if(self.get('menu')):
199      return
200    if(self.get('centred') and self.get('ownpos')['valid']):
201      return
202    self.modules['projection'].nudge(dx,dy)
203    self.forceRedraw()
204 
205  def zoom(self,dx):
206    """Handle dragging left/right along top of the screen to zoom"""
207    self.modules['projection'].nudgeZoom(-1 * dx / self.rect.width)
208    self.forceRedraw()
209
210  def handleDrag(self,x,y,dx,dy,startX,startY):
211    if(self.modules['overlay'].fullscreen()):
212      if(self.modules['overlay'].handleDrag(dx,dy,startX,startY)):
213        self.forceRedraw()
214    elif(self.get('sketch',0)):
215      self.m['sketch'].moveTo(x,y)
216      return
217    else:
218      self.move(dx,dy)
219   
220
221  def draw(self, cr):
222    start = clock()
223    proj = self.modules['projection']
224   
225    # Don't draw the map if a menu/overlay is fullscreen
226    if(not self.modules['overlay'].fullscreen()):
227     
228      # Map as image
229      self.modules['tiles'].draw(cr)
230
231      # The route
232      if(self.modules['route'].valid()):
233        cr.set_source_rgba(0.5, 0.0, 0.0, 0.5)
234        cr.set_line_width(12)
235        count = 0
236        for i in self.modules['route'].route['route']:
237          x,y = proj.ll2xy(i[0],i[1])
238          if(count == 0):
239            cr.move_to(x,y)
240          else:
241            cr.line_to(x,y)
242          count = count + 1
243        cr.stroke()
244     
245      # Each plugin can display on the map
246      for name,source in self.modules['poi'].items():
247        for group in source.groups:
248          for item in group.items:
249            x,y = proj.ll2xy(item.lat, item.lon)
250            if(proj.onscreen(x,y)):
251              cr.set_source_rgb(0.0, 0.4, 0.0)
252              cr.set_font_size(12)
253              cr.move_to(x,y)
254              cr.show_text(item.title)
255              cr.stroke()
256
257      self.m['sketch'].draw(cr,proj)
258      self.m['tracklog'].draw(cr,proj)
259     
260      pos = self.get('ownpos')
261      if(pos['valid']):
262        # Us
263        x,y = proj.ll2xy(pos['lat'], pos['lon'])
264        cr.set_source_rgb(0.0, 0.0, 0.0)
265        cr.arc(x,y,14, 0,2*3.1415)
266        cr.fill()
267        cr.set_source_rgb(1.0, 0.0, 0.0)
268        cr.arc(x,y,10, 0,2*3.1415)
269        cr.fill()
270       
271    # Overlay (menus etc)
272    self.modules['overlay'].draw(cr, self.rect)
273   
274    #print "Map took %1.2f ms" % (1000 * (clock() - start))
275   
276  def do_realize(self):
277    self.set_flags(self.flags() | gtk.REALIZED)
278    self.window = gdk.Window( \
279      self.get_parent_window(),
280      width = self.allocation.width,
281      height = self.allocation.height,
282      window_type = gdk.WINDOW_CHILD,
283      wclass = gdk.INPUT_OUTPUT,
284      event_mask = self.get_events() | gdk.EXPOSURE_MASK)
285    self.window.set_user_data(self)
286    self.style.attach(self.window)
287    self.style.set_background(self.window, gtk.STATE_NORMAL)
288    self.window.move_resize(*self.allocation)
289  def do_size_request(self, allocation):
290    pass
291  def do_size_allocate(self, allocation):
292    self.allocation = allocation
293    if self.flags() & gtk.REALIZED:
294      self.window.move_resize(*allocation)
295  def _expose_cairo(self, event, cr):
296    self.rect = self.allocation
297    self.modules['projection'].setView( \
298      self.rect.x, 
299      self.rect.y, 
300      self.rect.width, 
301      self.rect.height)
302    self.draw(cr)
303  def do_expose_event(self, event):
304    self.chain(event)
305    cr = self.window.cairo_create()
306    return self._expose_cairo(event, cr)
307
308class GuiBase:
309  """Wrapper class for a GUI interface"""
310  def __init__(self):
311    # Create the window
312    win = gtk.Window()
313    win.set_title('pyroute')
314    win.connect('delete-event', gtk.main_quit)
315    win.resize(480,640)
316    win.move(50, gtk.gdk.screen_height() - 650)
317   
318    # Events
319    event_box = gtk.EventBox()
320    event_box.connect("button_press_event", lambda w,e: self.pressed(e))
321    event_box.connect("button_release_event", lambda w,e: self.released(e))
322    event_box.connect("motion_notify_event", lambda w,e: self.moved(e))
323    win.add(event_box)
324   
325    # Create the map
326    self.mapWidget = MapWidget()
327    event_box.add(self.mapWidget)
328   
329    # Finalise the window
330    win.show_all()
331    gtk.main()
332   
333  def pressed(self, event):
334    self.dragstartx = event.x
335    self.dragstarty = event.y
336    #print dir(event)
337    #print "Pressed button %d at %1.0f, %1.0f" % (event.button, event.x, event.y)
338   
339    self.dragx = event.x
340    self.dragy = event.y
341    self.mapWidget.mousedown(event.x,event.y)
342   
343  def moved(self, event):
344    """Drag-handler"""
345
346    self.mapWidget.handleDrag( \
347      event.x,
348      event.y,
349      event.x - self.dragx, 
350      event.y - self.dragy,
351      self.dragstartx,
352      self.dragstarty)
353
354    self.dragx = event.x
355    self.dragy = event.y
356  def released(self, event):
357    dx = event.x - self.dragstartx
358    dy = event.y - self.dragstarty
359    distSq = dx * dx + dy * dy
360    if distSq < 4:
361      self.mapWidget.click(event.x, event.y)
362
363
364if __name__ == "__main__":
365  program = GuiBase()
Note: See TracBrowser for help on using the repository browser.