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

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

Make waypoints a plugin, same as RSS etc.
Rename plugins to 'poi' (since they're all point data)
Make poi layers map modules (so they get access to global data)
Add waypoint viewer to menu

  • 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 mod_waypoints import waypointsModule
60from base import pyrouteModule
61from tiles import tileHandler
62
63def update(mapWidget):
64  mapWidget.updatePosition();
65  return(True)
66
67class MapWidget(gtk.Widget, pyrouteModule):
68  __gsignals__ = { \
69    'realize': 'override',
70    'expose-event' : 'override',
71    'size-allocate': 'override',
72    'size-request': 'override'}
73  def __init__(self):
74    gtk.Widget.__init__(self)
75    self.draw_gc = None
76    self.timer = gobject.timeout_add(30, update, self)
77   
78    self.modules = {'poi':{}}
79    pyrouteModule.__init__(self, self.modules)
80   
81    #self.modules['poi']['rss'] = geoRss('Setup/feeds.txt')
82    #self.modules['poi']['geonames'] = geonames()
83    self.modules['poi']['waypoints'] = waypointsModule(self.modules, "data/waypoints.gpx")
84    self.modules['overlay'] = guiOverlay(self.modules)
85    self.modules['position'] = geoPosition()
86    self.modules['tiles'] = tileHandler(self.modules)
87    self.modules['data'] = DataStore(self, self.modules)
88    self.set('mode','cycle')
89    self.set('centred',False)
90    self.modules['osmdata'] = LoadOsm(None)
91    self.modules['projection'] = Projection()
92    self.modules['projection'].recentre(51.2,-0.2, 0.1)
93    self.modules['route'] = RouteOrDirect(self.modules['osmdata'])
94    self.updatePosition()
95    self.set('ownpos', {'valid':False})
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['poi'].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.