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

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

Move GPX loading function into GPX library
+ use it to load sketch files from disk

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