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

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

Some more stuff going into a base class (so e.g.
self.modulesdata?.getOption('x') is now just self.get('x')

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