11 class background_map ( gtk.gdk.Pixmap ) :
12 """Pixmap to hold a background map big enough for screen size
13 The minimal pixmap is calculated to hold an integer number of tiles as large
14 as required to cover the requested screen size. The number of tiles is rounded
15 up to an odd number, to clearly know which one is the central tile.
16 Although not strictly required for visualization, a tiles border is added,
17 whose primary purpose is to keep elements drawn in the pixmap when they get
18 scrolled out, to get better visualization if they are later scrolled in.
19 The object also stores a reference pixel that can be retrieved with method
20 get_viewport(), and gives the topleft/base pixel to extract the viewable
21 area (the reference pixel is actually encapsulated in the inner tile loader).
24 def __init__ ( self , map_size , tileloader ) :
27 self.tileloader = tileloader
29 # Values for minimun fit without border
30 center = map( lambda x : int( math.ceil( x / float(tileloader.tilesize) ) / 2 ) , map_size )
31 size = map( lambda x : 2 * x + 1 , center )
33 self.center = map( lambda x : x + bordersize , center )
34 self.size = map( lambda x : x + 2 * bordersize , size )
35 pixsize = map( lambda x : x * tileloader.tilesize , self.size )
37 # FIXME : seems that reproducing the previous behaviour requires an extra subtraction of 128 to vpor[1]
38 # when moving to non-integer viewports, the shift turns and adition, and changes to vpor[0]
39 self.__vport_base = bordersize * tileloader.tilesize , bordersize * tileloader.tilesize
41 gtk.gdk.Pixmap.__init__( self , None , pixsize[0] , pixsize[1] , 24 )
43 self.fill = map( lambda x : False , range( self.size[0] * self.size[1] ) )
47 def index ( self , x , y ) :
48 return x + y * self.size[0]
50 def get_vport_base ( self ) :
51 return self.__vport_base
53 # FIXME : This implementation does not account for the requested screen size, so don't give the right pixel
54 def get_viewport ( self ) :
55 refpix = self.tileloader.get_refpix()
56 return self.__vport_base[0] + refpix[0] , self.__vport_base[1] + refpix[1]
58 def reload ( self , config ) :
59 self.tileloader.set_params( config )
60 for i in range( self.size[0] * self.size[1] ) :
64 def loadtiles ( self ) :
66 for x in range(self.size[0]) :
67 for y in range(self.size[1]) :
69 if not self.fill[ self.index(x,y) ] :
70 pixbuf = self.tileloader.get_tile( (x-self.center[0],y-self.center[1]) )
72 self.fill[ self.index(x,y) ] = True
74 pixbuf = self.tileloader.emptytile()
76 dest_x = self.tileloader.tilesize * x
77 dest_y = self.tileloader.tilesize * y
78 self.draw_pixbuf( None , pixbuf , 0 , 0 , dest_x , dest_y )
81 def do_change_refpix ( self , dx , dy ) :
82 dx , dy = self.tileloader.do_change_refpix( dx , dy )
84 self.do_change_reftile( dx , dy )
86 def do_change_reftile( self , dx , dy ) :
87 self.tileloader.do_change_reftile( dx , dy )
89 pixsize = self.get_size()
90 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, pixsize[0] , pixsize[1] )
91 pixbuf.get_from_drawable( self , self.get_colormap() , 0 , 0 , 0 , 0 , pixsize[0] , pixsize[1] )
93 # width , source , destination
94 x_vals = [ pixsize[0] , 0 , 0 ]
95 y_vals = [ pixsize[1] , 0 , 0 ]
98 x_vals[0] -= abs(dx) * self.tileloader.tilesize
99 x_vals[cmp(dx,0)] = abs(dx) * self.tileloader.tilesize
101 for x in range(1,1+dx) :
102 for y in range(self.size[1]) : self.fill[ self.index(self.size[0]-x,y) ] = False
104 for x in range(1,1-dx) :
105 for y in range(self.size[1]) : self.fill[ self.index(x,y) ] = False
107 y_vals[0] -= abs(dy) * self.tileloader.tilesize
108 y_vals[cmp(dy,0)] = abs(dy) * self.tileloader.tilesize
110 for y in range(1,1+dy) :
111 for x in range(self.size[0]) : self.fill[ self.index(x,self.size[1]-y) ] = False
113 for y in range(1,1-dy) :
114 for x in range(self.size[0]) : self.fill[ self.index(x,y) ] = False
116 self.draw_pixbuf( None , pixbuf , x_vals[1] , y_vals[1] , x_vals[-1] , y_vals[-1] , x_vals[0] , y_vals[0] )
122 def __init__ ( self , conf ) :
124 self.rootbase = os.path.join( conf.mapsdir , conf.mapclass )
125 self.set_params( conf )
127 def set_params ( self , conf ) :
128 self.rootdir = os.path.join( self.rootbase , str(conf.zoom) )
129 self.__reftile , self.__refpix = self.get_reference( conf )
130 self.__zoom = conf.zoom
132 def do_change_refpix ( self , dx , dy ) :
133 self.__refpix[0] += dx
134 self.__refpix[1] += dy
135 tileshift = self.__refpix[0] / self.tilesize , self.__refpix[1] / self.tilesize
136 self.__refpix[0] %= self.tilesize
137 self.__refpix[1] %= self.tilesize
140 def do_change_reftile( self , dx , dy ) :
141 self.__reftile[0] += dx
142 self.__reftile[1] += dy
144 def get_reftile ( self ) :
145 return self.__reftile
147 def get_refpix ( self ) :
150 def get_reference ( self , conf ) :
151 tilex = self.lon2tilex( conf.lon , conf.zoom )
152 tiley = self.lat2tiley( conf.lat , conf.zoom )
153 tile = tilex[1] , tiley[1]
154 pix = tilex[0] , tiley[0]
155 return map( int , tile ) , map( lambda x : int( self.tilesize * x ) , pix )
157 def lon2tilex ( self , lon , zoom ) :
158 return math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
160 def lat2tiley ( self , lat , zoom ) :
161 lat = lat * math.pi / 180
162 return math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
164 def get_latlon ( self ) :
165 pixx , pixy = map( float , self.__refpix )
166 tilex , tiley = map( float , self.__reftile )
167 tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tilesize ) / 2.0 ** self.__zoom )
168 return math.degrees( math.atan( math.sinh( tiley ) ) ) , ( tilex + pixx/self.tilesize ) / 2.0 ** self.__zoom * 360.0 - 180.0
170 def get_tile ( self , tile ) :
171 file = self.tilepath( self.__reftile[0] + tile[0] , self.__reftile[1] + tile[1] )
174 return gtk.gdk.pixbuf_new_from_file( file )
177 # useful members : response.code, response.headers
178 response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
179 if response.geturl() != "http://tile.openstreetmap.org/11/0/0.png" :
180 fd = open( file , 'w' )
181 fd.write( response.read() )
183 # FIXME : can this actually produce a gobject.GError exception ?
184 return gtk.gdk.pixbuf_new_from_file( file )
190 def tilepath( self , tilex , tiley ) :
191 return "%s/%s/%s.png" % ( self.rootdir , tilex , tiley )
193 def emptytile( self ) :
194 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize )
195 pixbuf.fill( 0x00000000 )
199 class AbstractmapWidget :
201 def __init__ ( self , config , map_size ) :
205 # Maximum width should be 800, but actually gets reduced
206 self.win_x , self.win_y = map_size
209 self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
210 self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
212 def recenter ( self , latlon ) :
214 center = self.gps2pix( latlon , self.center() )
215 pixel = self.gps2pix( (self.conf.lat,self.conf.lon) , self.center() )
217 distance = math.sqrt( (pixel[0]-center[0])**2 + (pixel[1]-center[1])**2 )
219 # FIXME : instead of hardcoded, should depend on the actual display size
221 self.conf.set_latlon( latlon )
223 self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
224 self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
228 def tilex2lon ( self , ( tilex , pixx ) , zoom ) :
231 return ( tilex + pixx/self.tile_size ) / 2.0 ** zoom * 360.0 - 180.0
233 def tiley2lat ( self , ( tiley , pixy ) , zoom ) :
236 tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tile_size ) / 2.0 ** zoom )
237 return math.degrees( math.atan( math.sinh( tiley ) ) )
239 def SetZoom( self , zoom ) :
241 lat = self.tiley2lat( ( self.reftile_y , self.refpix_y ) , self.conf.zoom )
242 lon = self.tilex2lon( ( self.reftile_x , self.refpix_x ) , self.conf.zoom )
243 self.reftile_x , self.refpix_x = self.lon2tilex( lon , zoom )
244 self.reftile_y , self.refpix_y = self.lat2tiley( lat , zoom )
245 self.conf.set_zoom( zoom )
249 def lon2tilex ( self , lon , zoom ) :
250 number = math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
251 return int( number[1] ) , int( self.tile_size * number[0] )
253 def lat2tiley ( self , lat , zoom ) :
254 lat = lat * math.pi / 180
255 number = math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
256 return int( number[1] ) , int( self.tile_size * number[0] )
258 def gps2pix ( self , ( lat , lon ) , ( center_x , center_y ) ) :
260 x_pos = self.lon2tilex( lon , self.conf.zoom )
261 y_pos = self.lat2tiley( lat , self.conf.zoom )
263 dest_x = self.tile_size * ( x_pos[0] - self.reftile_x ) + center_x + x_pos[1]
264 dest_y = self.tile_size * ( y_pos[0] - self.reftile_y ) + center_y + y_pos[1]
266 return dest_x , dest_y
268 def tilename ( self , x , y , zoom ) :
269 file = self.tile2file( self.reftile_x + x , self.reftile_y + y , zoom )
276 # useful members : response.code, response.headers
277 response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
278 if response.geturl() == "http://tile.openstreetmap.org/11/0/0.png" :
280 fd = open( file , 'w' )
281 fd.write( response.read() )
289 def tile2file( self , tilex , tiley , zoom ) :
290 rootdir = "%s/%s/%s" % ( self.conf.mapsdir , self.conf.mapclass , zoom )
291 if not os.path.isdir( rootdir ) :
293 rootsubdir = "%s/%s" % ( rootdir , tilex )
294 if not os.path.isdir( rootsubdir ) :
296 return "%s/%s.png" % ( rootsubdir , tiley )
299 class simpleMapWidget ( AbstractmapWidget , gtk.Image ) :
301 def __init__ ( self , config , map_size=(800,480) ) :
302 AbstractmapWidget.__init__( self , config , map_size )
304 gtk.Image.__init__(self)
306 self._bg = background_map( map_size , tile_loader( config ) )
309 self.update_background()
311 def update_background( self ) :
312 vport = self._bg.get_viewport()
313 self.reftile_x , self.reftile_y = self._bg.tileloader.get_reftile()
314 p = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.win_x , self.win_y )
315 p.get_from_drawable( self._bg , self._bg.get_colormap() , vport[0] , vport[1] , 0 , 0 , self.win_x , self.win_y )
317 self.set_from_pixbuf(p)
319 def do_change_refpix ( self , dx , dy ) :
320 self._bg.do_change_refpix( dx , dy )
321 self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
322 self.update_background()
324 def do_change_reftile ( self , dx , dy ) :
325 self._bg.do_change_reftile( dx , dy )
326 self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
327 self.update_background()
329 def do_change_zoomlevel ( self , dz ) :
330 self.config.zoom += dz
331 self._bg.reload( self.config )
332 self.update_background()
334 def composeMap( self ) :
335 center_x , center_y = self.center()
337 # Ranges should be long enough as to fill the screen
338 # Maybe they should be decided based on self.win_x, self.win_y
339 for i in range(-3,4) :
340 for j in range(-3,4) :
341 file = self.tilename( i , j , self.conf.zoom )
343 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
344 pixbuf.fill( 0x00000000 )
347 pixbuf = gtk.gdk.pixbuf_new_from_file( file )
348 except gobject.GError , ex :
349 print "Corrupted file %s" % ( file )
351 #file = self.tilename( self.reftile_x + i , self.reftile_y + j , self.conf.zoom )
352 file = self.tilename( i , j , self.conf.zoom )
354 pixbuf = gtk.gdk.pixbuf_new_from_file( file )
356 print "Total failure for tile for %s,%s" % ( self.reftile_x + i , self.reftile_y + j )
357 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
359 dest_x = self.tile_size * i + center_x
360 dest_y = self.tile_size * j + center_y
363 size_x = self.tile_size
366 size_x = self.tile_size + dest_x
368 if dest_x + self.tile_size > self.win_x :
369 size_x = self.win_x - dest_x
372 size_y = self.tile_size
375 size_y = self.tile_size + dest_y
377 if dest_y + self.tile_size > self.win_y :
378 size_y = self.win_y - dest_y
380 if ( size_x > 0 and size_y > 0 ) and ( init_x < self.tile_size and init_y < self.tile_size ) :
381 pixbuf.copy_area( init_x, init_y, size_x, size_y, self.get_pixbuf(), dest_x , dest_y )
386 vport = self._bg.get_vport_base()
387 center_x = self._bg.center[0]*self._bg.tileloader.tilesize - vport[0]
388 center_y = self._bg.center[1]*self._bg.tileloader.tilesize - vport[1]
390 # To get the central pixel in the window center, we must shift to the tile origin
391 center_x -= self.refpix_x
392 center_y -= self.refpix_y
394 return center_x , center_y
396 def plot( self , pixmap , coords , colorname , radius=1 ) :
398 center_x , center_y = self.center()
401 gc.foreground = pixmap.get_colormap().alloc_color( colorname )
403 dest_x , dest_y = self.gps2pix( coords , ( center_x , center_y ) )
404 pixmap.draw_rectangle(gc, True , dest_x-radius , dest_y-radius , 2*radius+1 , 2*radius+1 )
406 def draw_paths( self , pixbuf ) :
408 pixmap,mask = pixbuf.render_pixmap_and_mask()
410 filename = "/tmp/wiscan_gui.info"
411 fd = open( filename )
412 for line in fd.readlines() :
413 values = line.split()
414 if values[1] == "FIX" :
415 self.plot( pixmap , ( float(values[5]) , float(values[6]) ) , "red" )
418 pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
420 def plot_APs( self , pixbuf ) :
422 pixmap,mask = pixbuf.render_pixmap_and_mask()
424 db = wifimap.db.database( os.path.join( self.conf.homedir , self.conf.dbname ) )
426 # NOTE : Intervals for query are just educated guesses to fit in window
427 lat , lon = self.conf.lat , self.conf.lon
428 for ap in db.db.execute( "SELECT * FROM ap where lat/n>%f and lat/n<%f and lon/n>%f and lon/n<%f" % ( lat - 0.003 , lat + 0.003 , lon - 0.007 , lon + 0.007 ) ) :
430 self.plot( pixmap , ( ap[4]/ap[3] , ap[5]/ap[3] ) , "blue" )
433 pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
435 class mapWidget ( gtk.EventBox ) :
437 def __init__ ( self , config ) :
438 gtk.EventBox.__init__( self )
439 self.mapwidget = simpleMapWidget( config )
440 self.add( self.mapwidget )
442 self.click_x , self.click_y = None , None
443 self.set_events( gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK )
444 self.connect_object('button_press_event', self.press_event, self.mapwidget)
445 self.connect_object('button_release_event', self.release_event, self.mapwidget)
447 def press_event ( self , widget , event ) :
448 self.click_x , self.click_y = event.x , event.y
450 def release_event ( self , widget, event ) :
451 # NOTE : we use origin-current for deltas because the map center moves in the opposite direction
452 delta_x = int( self.click_x - event.x )
453 delta_y = int( self.click_y - event.y )
454 widget.do_change_refpix(delta_x, delta_y)
455 self.click_x , self.click_y = None , None
457 if __name__ == "__main__" :
459 class StaticConfiguration :
461 def __init__ ( self , type=None ) :
464 self.homedir , self.dbname = None , None
465 self.mapsdir , self.mapclass = os.path.join( os.environ['HOME'] , "MyDocs/.maps" ) , "OpenStreetMap I"
467 self.store_log , self.use_mapper , self.store_gps = None , None , None
469 self.lat , self.lon = 40.416 , -3.683
472 def on_key_press ( widget, event , map ) :
473 if event.keyval == gtk.keysyms.Up :
474 map.do_change_reftile(0,-1)
475 elif event.keyval == gtk.keysyms.Down :
476 map.do_change_reftile( 0 , +1 )
477 elif event.keyval == gtk.keysyms.Right :
478 map.do_change_reftile( +1 , 0 )
479 elif event.keyval == gtk.keysyms.Left :
480 map.do_change_reftile( -1 , 0 )
481 elif event.keyval == 49 :
482 map.do_change_zoomlevel( -1 )
483 elif event.keyval == 50 :
484 map.do_change_zoomlevel( +1 )
486 print "UNKNOWN",event.keyval
488 config = StaticConfiguration()
489 mapwidget = mapWidget( config )
490 window = gtk.Window()
491 window.connect("destroy", gtk.main_quit )
492 window.connect("key-press-event", on_key_press, mapwidget.mapwidget )
493 window.add( mapwidget )