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 # FIXME : This implementation does not account for the requested screen size, so don't give the right pixel
51 def get_viewport ( self ) :
52 refpix = self.tileloader.get_refpix()
53 return self.__vport_base[0] + refpix[0] , self.__vport_base[1] + refpix[1]
55 def reload ( self , config ) :
56 self.tileloader.set_params( config )
57 for i in range( self.size[0] * self.size[1] ) :
61 def loadtiles ( self ) :
63 for x in range(self.size[0]) :
64 for y in range(self.size[1]) :
66 if not self.fill[ self.index(x,y) ] :
67 pixbuf = self.tileloader.get_tile( (x-self.center[0],y-self.center[1]) )
69 self.fill[ self.index(x,y) ] = True
71 pixbuf = self.tileloader.emptytile()
73 dest_x = self.tileloader.tilesize * x
74 dest_y = self.tileloader.tilesize * y
75 self.draw_pixbuf( None , pixbuf , 0 , 0 , dest_x , dest_y )
78 def do_change_refpix ( self , dx , dy ) :
79 dx , dy = self.tileloader.do_change_refpix( dx , dy )
81 self.do_change_reftile( dx , dy )
83 def do_change_reftile( self , dx , dy ) :
84 self.tileloader.do_change_reftile( dx , dy )
86 pixsize = self.get_size()
87 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, pixsize[0] , pixsize[1] )
88 pixbuf.get_from_drawable( self , self.get_colormap() , 0 , 0 , 0 , 0 , pixsize[0] , pixsize[1] )
90 # width , source , destination
91 x_vals = [ pixsize[0] , 0 , 0 ]
92 y_vals = [ pixsize[1] , 0 , 0 ]
95 x_vals[0] -= abs(dx) * self.tileloader.tilesize
96 x_vals[cmp(dx,0)] = abs(dx) * self.tileloader.tilesize
98 for x in range(1,1+dx) :
99 for y in range(self.size[1]) : self.fill[ self.index(self.size[0]-x,y) ] = False
101 for x in range(1,1-dx) :
102 for y in range(self.size[1]) : self.fill[ self.index(x,y) ] = False
104 y_vals[0] -= abs(dy) * self.tileloader.tilesize
105 y_vals[cmp(dy,0)] = abs(dy) * self.tileloader.tilesize
107 for y in range(1,1+dy) :
108 for x in range(self.size[0]) : self.fill[ self.index(x,self.size[1]-y) ] = False
110 for y in range(1,1-dy) :
111 for x in range(self.size[0]) : self.fill[ self.index(x,y) ] = False
113 self.draw_pixbuf( None , pixbuf , x_vals[1] , y_vals[1] , x_vals[-1] , y_vals[-1] , x_vals[0] , y_vals[0] )
119 def __init__ ( self , conf ) :
121 self.rootbase = os.path.join( conf.mapsdir , conf.mapclass )
122 self.set_params( conf )
124 def set_params ( self , conf ) :
125 self.rootdir = os.path.join( self.rootbase , str(conf.zoom) )
126 self.__reftile , self.__refpix = self.get_reference( conf )
127 self.__zoom = conf.zoom
129 def do_change_refpix ( self , dx , dy ) :
130 self.__refpix[0] += dx
131 self.__refpix[1] += dy
132 tileshift = self.__refpix[0] / self.tilesize , self.__refpix[1] / self.tilesize
133 self.__refpix[0] %= self.tilesize
134 self.__refpix[1] %= self.tilesize
137 def do_change_reftile( self , dx , dy ) :
138 self.__reftile[0] += dx
139 self.__reftile[1] += dy
141 def get_reftile ( self ) :
142 return self.__reftile
144 def get_refpix ( self ) :
147 def get_reference ( self , conf ) :
148 tilex = self.lon2tilex( conf.lon , conf.zoom )
149 tiley = self.lat2tiley( conf.lat , conf.zoom )
150 tile = tilex[1] , tiley[1]
151 pix = tilex[0] , tiley[0]
152 return map( int , tile ) , map( lambda x : int( self.tilesize * x ) , pix )
154 def lon2tilex ( self , lon , zoom ) :
155 return math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
157 def lat2tiley ( self , lat , zoom ) :
158 lat = lat * math.pi / 180
159 return math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
161 def get_latlon ( self ) :
162 pixx , pixy = map( float , self.__refpix )
163 tilex , tiley = map( float , self.__reftile )
164 tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tilesize ) / 2.0 ** self.__zoom )
165 return math.degrees( math.atan( math.sinh( tiley ) ) ) , ( tilex + pixx/self.tilesize ) / 2.0 ** self.__zoom * 360.0 - 180.0
167 def get_tile ( self , tile ) :
168 file = self.tilepath( self.__reftile[0] + tile[0] , self.__reftile[1] + tile[1] )
171 return gtk.gdk.pixbuf_new_from_file( file )
174 # useful members : response.code, response.headers
175 response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
176 if response.geturl() != "http://tile.openstreetmap.org/11/0/0.png" :
177 fd = open( file , 'w' )
178 fd.write( response.read() )
180 # FIXME : can this actually produce a gobject.GError exception ?
181 return gtk.gdk.pixbuf_new_from_file( file )
187 def tilepath( self , tilex , tiley ) :
188 return "%s/%s/%s.png" % ( self.rootdir , tilex , tiley )
190 def emptytile( self ) :
191 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize )
192 pixbuf.fill( 0x00000000 )
196 class AbstractmapWidget :
198 def __init__ ( self , config , map_size ) :
202 # Maximum width should be 800, but actually gets reduced
203 self.win_x , self.win_y = map_size
206 self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
207 self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
209 def recenter ( self , latlon ) :
211 center = self.gps2pix( latlon , self.center() )
212 pixel = self.gps2pix( (self.conf.lat,self.conf.lon) , self.center() )
214 distance = math.sqrt( (pixel[0]-center[0])**2 + (pixel[1]-center[1])**2 )
216 # FIXME : instead of hardcoded, should depend on the actual display size
218 self.conf.set_latlon( latlon )
220 self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
221 self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
225 def tilex2lon ( self , ( tilex , pixx ) , zoom ) :
228 return ( tilex + pixx/self.tile_size ) / 2.0 ** zoom * 360.0 - 180.0
230 def tiley2lat ( self , ( tiley , pixy ) , zoom ) :
233 tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tile_size ) / 2.0 ** zoom )
234 return math.degrees( math.atan( math.sinh( tiley ) ) )
236 def SetZoom( self , zoom ) :
238 lat = self.tiley2lat( ( self.reftile_y , self.refpix_y ) , self.conf.zoom )
239 lon = self.tilex2lon( ( self.reftile_x , self.refpix_x ) , self.conf.zoom )
240 self.reftile_x , self.refpix_x = self.lon2tilex( lon , zoom )
241 self.reftile_y , self.refpix_y = self.lat2tiley( lat , zoom )
242 self.conf.set_zoom( zoom )
246 def lon2tilex ( self , lon , zoom ) :
247 number = math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
248 return int( number[1] ) , int( self.tile_size * number[0] )
250 def lat2tiley ( self , lat , zoom ) :
251 lat = lat * math.pi / 180
252 number = math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
253 return int( number[1] ) , int( self.tile_size * number[0] )
255 def gps2pix ( self , ( lat , lon ) , ( center_x , center_y ) ) :
257 x_pos = self.lon2tilex( lon , self.conf.zoom )
258 y_pos = self.lat2tiley( lat , self.conf.zoom )
260 dest_x = self.tile_size * ( x_pos[0] - self.reftile_x ) + center_x + x_pos[1]
261 dest_y = self.tile_size * ( y_pos[0] - self.reftile_y ) + center_y + y_pos[1]
263 return dest_x , dest_y
265 def tilename ( self , x , y , zoom ) :
266 file = self.tile2file( self.reftile_x + x , self.reftile_y + y , zoom )
273 # useful members : response.code, response.headers
274 response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
275 if response.geturl() == "http://tile.openstreetmap.org/11/0/0.png" :
277 fd = open( file , 'w' )
278 fd.write( response.read() )
286 def tile2file( self , tilex , tiley , zoom ) :
287 rootdir = "%s/%s/%s" % ( self.conf.mapsdir , self.conf.mapclass , zoom )
288 if not os.path.isdir( rootdir ) :
290 rootsubdir = "%s/%s" % ( rootdir , tilex )
291 if not os.path.isdir( rootsubdir ) :
293 return "%s/%s.png" % ( rootsubdir , tiley )
296 class simpleMapWidget ( AbstractmapWidget , gtk.Image ) :
298 def __init__ ( self , config , map_size=(800,480) ) :
299 AbstractmapWidget.__init__( self , config , map_size )
301 gtk.Image.__init__(self)
303 self._bg = background_map( map_size , tile_loader( config ) )
306 self.update_background()
308 def update_background( self ) :
309 vport = self._bg.get_viewport()
310 self.reftile_x , self.reftile_y = self._bg.tileloader.get_reftile()
311 p = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.win_x , self.win_y )
312 p.get_from_drawable( self._bg , self._bg.get_colormap() , vport[0] , vport[1] , 0 , 0 , self.win_x , self.win_y )
314 self.set_from_pixbuf(p)
316 def do_change_refpix ( self , dx , dy ) :
317 self._bg.do_change_refpix( dx , dy )
318 self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
319 self.update_background()
321 def do_change_reftile ( self , dx , dy ) :
322 self._bg.do_change_reftile( dx , dy )
323 self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
324 self.update_background()
326 def do_change_zoomlevel ( self , dz ) :
327 self.config.zoom += dz
328 self._bg.reload( self.config )
329 self.update_background()
331 def composeMap( self ) :
332 center_x , center_y = self.center()
334 # Ranges should be long enough as to fill the screen
335 # Maybe they should be decided based on self.win_x, self.win_y
336 for i in range(-3,4) :
337 for j in range(-3,4) :
338 file = self.tilename( i , j , self.conf.zoom )
340 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
341 pixbuf.fill( 0x00000000 )
344 pixbuf = gtk.gdk.pixbuf_new_from_file( file )
345 except gobject.GError , ex :
346 print "Corrupted file %s" % ( file )
348 #file = self.tilename( self.reftile_x + i , self.reftile_y + j , self.conf.zoom )
349 file = self.tilename( i , j , self.conf.zoom )
351 pixbuf = gtk.gdk.pixbuf_new_from_file( file )
353 print "Total failure for tile for %s,%s" % ( self.reftile_x + i , self.reftile_y + j )
354 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
356 dest_x = self.tile_size * i + center_x
357 dest_y = self.tile_size * j + center_y
360 size_x = self.tile_size
363 size_x = self.tile_size + dest_x
365 if dest_x + self.tile_size > self.win_x :
366 size_x = self.win_x - dest_x
369 size_y = self.tile_size
372 size_y = self.tile_size + dest_y
374 if dest_y + self.tile_size > self.win_y :
375 size_y = self.win_y - dest_y
377 if ( size_x > 0 and size_y > 0 ) and ( init_x < self.tile_size and init_y < self.tile_size ) :
378 pixbuf.copy_area( init_x, init_y, size_x, size_y, self.get_pixbuf(), dest_x , dest_y )
383 center_x , center_y = self.win_x / 2 , self.win_y / 2
385 # To get the central pixel in the window center, we must shift to the tile origin
386 center_x -= self.refpix_x
387 center_y -= self.refpix_y
389 return center_x , center_y
391 def plot( self , pixmap , coords , colorname , radius=3 ) :
393 center_x , center_y = self.center()
396 gc.foreground = pixmap.get_colormap().alloc_color( colorname )
398 dest_x , dest_y = self.gps2pix( coords , ( center_x , center_y ) )
399 pixmap.draw_rectangle(gc, True , dest_x , dest_y , radius , radius )
401 def draw_paths( self , pixbuf ) :
403 pixmap,mask = pixbuf.render_pixmap_and_mask()
405 filename = "/tmp/wiscan_gui.info"
406 fd = open( filename )
407 for line in fd.readlines() :
408 values = line.split()
409 if values[1] == "FIX" :
410 self.plot( pixmap , ( float(values[5]) , float(values[6]) ) , "red" )
413 pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
415 def plot_APs( self , pixbuf ) :
417 pixmap,mask = pixbuf.render_pixmap_and_mask()
419 db = wifimap.db.database( os.path.join( self.conf.homedir , self.conf.dbname ) )
421 # NOTE : Intervals for query are just educated guesses to fit in window
422 lat , lon = self.conf.lat , self.conf.lon
423 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 ) ) :
425 self.plot( pixmap , ( ap[4]/ap[3] , ap[5]/ap[3] ) , "blue" )
428 pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
430 class mapWidget ( gtk.EventBox ) :
432 def __init__ ( self , config ) :
433 gtk.EventBox.__init__( self )
434 self.mapwidget = simpleMapWidget( config )
435 self.add( self.mapwidget )
437 self.click_x , self.click_y = None , None
438 self.set_events( gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK )
439 self.connect_object('button_press_event', self.press_event, self.mapwidget)
440 self.connect_object('button_release_event', self.release_event, self.mapwidget)
442 def press_event ( self , widget , event ) :
443 self.click_x , self.click_y = event.x , event.y
445 def release_event ( self , widget, event ) :
446 # NOTE : we use origin-current for deltas because the map center moves in the opposite direction
447 delta_x = int( self.click_x - event.x )
448 delta_y = int( self.click_y - event.y )
449 widget.do_change_refpix(delta_x, delta_y)
450 self.click_x , self.click_y = None , None
452 if __name__ == "__main__" :
454 class StaticConfiguration :
456 def __init__ ( self , type=None ) :
459 self.homedir , self.dbname = None , None
460 self.mapsdir , self.mapclass = os.path.join( os.environ['HOME'] , "MyDocs/.maps" ) , "OpenStreetMap I"
462 self.store_log , self.use_mapper , self.store_gps = None , None , None
464 self.lat , self.lon = 40.416 , -3.683
467 def on_key_press ( widget, event , map ) :
468 if event.keyval == gtk.keysyms.Up :
469 map.do_change_reftile(0,-1)
470 elif event.keyval == gtk.keysyms.Down :
471 map.do_change_reftile( 0 , +1 )
472 elif event.keyval == gtk.keysyms.Right :
473 map.do_change_reftile( +1 , 0 )
474 elif event.keyval == gtk.keysyms.Left :
475 map.do_change_reftile( -1 , 0 )
476 elif event.keyval == 49 :
477 map.do_change_zoomlevel( -1 )
478 elif event.keyval == 50 :
479 map.do_change_zoomlevel( +1 )
481 print "UNKNOWN",event.keyval
483 config = StaticConfiguration()
484 mapwidget = mapWidget( config )
485 window = gtk.Window()
486 window.connect("destroy", gtk.main_quit )
487 window.connect("key-press-event", on_key_press, mapwidget.mapwidget )
488 window.add( mapwidget )