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

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

download on request

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