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

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

Move the map tile code into its own module

Add headers and usage instructions to a bunch of modules

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