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

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

initial test of a base-class to simplify stuff like getting data or sending events

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