Remove unused method composeMap
[wifihood] / view.py
1 #!/usr/bin/python
2
3 import gtk
4 import gobject
5
6 import urllib2
7 import math
8
9 import os
10
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).
22 """
23
24     def __init__ ( self , map_size , tileloader ) :
25         bordersize = 1
26
27         self.tileloader = tileloader
28
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 )
32
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 )
36
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
40
41         gtk.gdk.Pixmap.__init__( self , None , pixsize[0] , pixsize[1] , 24 )
42
43         self.fill = map( lambda x : False , range( self.size[0] * self.size[1] ) )
44
45         self.loadtiles()
46
47     def index ( self , x , y ) :
48         return x + y * self.size[0]
49
50     def get_vport_base ( self ) :
51         return self.__vport_base
52
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]
57
58     def reload ( self , config ) :
59         self.tileloader.set_params( config )
60         for i in range( self.size[0] * self.size[1] ) :
61             self.fill[i] = False
62         self.loadtiles()
63
64     def loadtiles ( self ) :
65
66         for x in range(self.size[0]) :
67             for y in range(self.size[1]) :
68
69               if not self.fill[ self.index(x,y) ] :
70                 pixbuf = self.tileloader.get_tile( (x-self.center[0],y-self.center[1]) )
71                 if pixbuf :
72                     self.fill[ self.index(x,y) ] = True
73                 else :
74                     pixbuf = self.tileloader.emptytile()
75
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 )
79
80     
81     def do_change_refpix ( self , dx , dy ) :
82         dx , dy = self.tileloader.do_change_refpix( dx , dy )
83         if dx or dy :
84             self.do_change_reftile( dx , dy )
85
86     def do_change_reftile( self , dx , dy ) :
87         self.tileloader.do_change_reftile( dx , dy )
88
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] )
92
93         # width , source , destination
94         x_vals = [ pixsize[0] , 0 , 0 ]
95         y_vals = [ pixsize[1] , 0 , 0 ]
96
97         if dx :
98             x_vals[0] -= abs(dx) * self.tileloader.tilesize
99             x_vals[cmp(dx,0)] = abs(dx) * self.tileloader.tilesize
100             if dx > 0 :
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
103             if dx < 0 :
104               for x in range(1,1-dx) :
105                 for y in range(self.size[1]) : self.fill[ self.index(x,y) ] = False
106         if dy :
107             y_vals[0] -= abs(dy) * self.tileloader.tilesize
108             y_vals[cmp(dy,0)] = abs(dy) * self.tileloader.tilesize
109             if dy > 0 :
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
112             if dy < 0 :
113               for y in range(1,1-dy) :
114                 for x in range(self.size[0]) : self.fill[ self.index(x,y) ] = False
115
116         self.draw_pixbuf( None , pixbuf , x_vals[1] , y_vals[1] , x_vals[-1] , y_vals[-1] , x_vals[0] , y_vals[0] )
117         self.loadtiles()
118
119
120 class tile_loader :
121
122     def __init__ ( self , conf ) :
123         self.tilesize = 256
124         self.rootbase = os.path.join( conf.mapsdir , conf.mapclass )
125         self.set_params( conf )
126
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
131
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
138         return tileshift
139
140     def do_change_reftile( self , dx , dy ) :
141         self.__reftile[0] += dx
142         self.__reftile[1] += dy
143
144     def get_reftile ( self ) :
145         return self.__reftile
146
147     def get_refpix ( self ) :
148         return self.__refpix
149
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 )
156
157     def lon2tilex ( self , lon , zoom ) :
158         return math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
159
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 )
163
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
169
170     def get_tile ( self , tile ) :
171         file = self.tilepath( self.__reftile[0] + tile[0] , self.__reftile[1] + tile[1] )
172         try :
173             os.stat(file)
174             return gtk.gdk.pixbuf_new_from_file( file )
175         except :
176             try :
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() )
182                     fd.close()
183                     # FIXME : can this actually produce a gobject.GError exception ?
184                     return gtk.gdk.pixbuf_new_from_file( file )
185             except :
186                 pass
187
188         return None
189
190     def tilepath( self , tilex , tiley ) :
191       return "%s/%s/%s.png" % ( self.rootdir , tilex , tiley )
192
193     def emptytile( self ) :
194         pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize )
195         pixbuf.fill( 0x00000000 )
196         return pixbuf
197
198
199 class AbstractmapWidget :
200
201   def __init__ ( self , config , map_size ) :
202
203     self.conf = config
204
205     # Maximum width should be 800, but actually gets reduced
206     self.win_x , self.win_y = map_size
207     self.tile_size = 256
208
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 )
211
212   def recenter ( self , latlon ) :
213
214         center = self.gps2pix( latlon , self.center() )
215         pixel = self.gps2pix( (self.conf.lat,self.conf.lon) , self.center() )
216
217         distance = math.sqrt( (pixel[0]-center[0])**2 + (pixel[1]-center[1])**2 )
218
219         # FIXME : instead of hardcoded, should depend on the actual display size
220         if distance > 150 :
221             self.conf.set_latlon( latlon )
222
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 )
225
226             self.update_background(True)
227
228   def tilex2lon ( self , ( tilex , pixx ) , zoom ) :
229         tilex = float(tilex)
230         pixx = float(pixx)
231         return ( tilex + pixx/self.tile_size ) / 2.0 ** zoom * 360.0 - 180.0
232
233   def tiley2lat ( self , ( tiley , pixy ) , zoom ) :
234         tiley = float(tiley)
235         pixy = float(pixy)
236         tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tile_size ) / 2.0 ** zoom )
237         return math.degrees( math.atan( math.sinh( tiley ) ) )
238
239   def SetZoom( self , zoom ) :
240         self.hide()
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 )
246         self.update_background(True)
247         self.show()
248
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] )
252
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] )
257
258   def gps2pix ( self , ( lat , lon ) , ( center_x , center_y ) ) :
259
260     x_pos = self.lon2tilex( lon , self.conf.zoom )
261     y_pos = self.lat2tiley( lat , self.conf.zoom )
262
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]
265
266     return dest_x , dest_y
267
268   def tilename ( self , x , y , zoom ) :
269     file = self.tile2file( self.reftile_x + x , self.reftile_y + y , zoom )
270     try :
271       os.stat(file)
272     except :
273     #  if mapDownload :
274       if False :
275         try :
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" :
279               return None
280           fd = open( file , 'w' )
281           fd.write( response.read() )
282           fd.close()
283         except :
284           return None
285       else :
286         return None
287     return file
288
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 ) :
292       os.mkdir(rootdir)
293     rootsubdir = "%s/%s" % ( rootdir , tilex )
294     if not os.path.isdir( rootsubdir ) :
295       os.mkdir(rootsubdir)
296     return "%s/%s.png" % ( rootsubdir , tiley )
297
298
299 class simpleMapWidget ( AbstractmapWidget , gtk.Image ) :
300
301     def __init__ ( self , config , map_size=(800,480) ) :
302         AbstractmapWidget.__init__( self , config , map_size )
303
304         gtk.Image.__init__(self)
305
306         self._bg = background_map( map_size , tile_loader( config ) )
307         self.config = config
308
309         self.update_background()
310     
311     def update_background( self , reload=False ) :
312         if reload :
313             self._bg.reload( self.conf )
314         vport = self._bg.get_viewport()
315         self.reftile_x , self.reftile_y = self._bg.tileloader.get_reftile()
316         p = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.win_x , self.win_y )
317         p.get_from_drawable( self._bg , self._bg.get_colormap() , vport[0] , vport[1] , 0 , 0 , self.win_x , self.win_y )
318         self.draw_paths(p)
319         self.set_from_pixbuf(p)
320     
321     def do_change_refpix ( self , dx , dy ) :
322         self._bg.do_change_refpix( dx , dy )
323         self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
324         self.update_background()
325
326     def do_change_reftile ( self , dx , dy ) :
327         self._bg.do_change_reftile( dx , dy )
328         self.config.lat , self.config.lon = self._bg.tileloader.get_latlon()
329         self.update_background()
330
331     def do_change_zoomlevel ( self , dz ) :
332         self.config.zoom += dz
333         self._bg.reload( self.config )
334         self.update_background()
335
336     def center( self ) :
337
338         vport = self._bg.get_vport_base()
339         center_x = self._bg.center[0]*self._bg.tileloader.tilesize - vport[0]
340         center_y = self._bg.center[1]*self._bg.tileloader.tilesize - vport[1]
341
342         # To get the central pixel in the window center, we must shift to the tile origin
343         center_x -= self.refpix_x
344         center_y -= self.refpix_y
345
346         return center_x , center_y
347
348     def plot( self , pixmap , coords , colorname , radius=1 ) :
349
350         center_x , center_y = self.center()
351
352         gc = pixmap.new_gc()
353         gc.foreground = pixmap.get_colormap().alloc_color( colorname )
354
355         dest_x , dest_y = self.gps2pix( coords , ( center_x , center_y ) )
356         pixmap.draw_rectangle(gc, True , dest_x-radius , dest_y-radius , 2*radius+1 , 2*radius+1 )
357
358     def draw_paths( self , pixbuf ) :
359
360         pixmap,mask = pixbuf.render_pixmap_and_mask()
361
362         filename = "/tmp/wiscan_gui.info"
363         fd = open( filename )
364         for line in fd.readlines() :
365             values = line.split()
366             if values[1] == "FIX" :
367                 self.plot( pixmap , ( float(values[5]) , float(values[6]) ) , "red" )
368         fd.close()
369
370         pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
371
372     def plot_APs( self , pixbuf ) :
373
374         pixmap,mask = pixbuf.render_pixmap_and_mask()
375
376         db = wifimap.db.database( os.path.join( self.conf.homedir , self.conf.dbname ) )
377         db.open()
378         # NOTE : Intervals for query are just educated guesses to fit in window
379         lat , lon = self.conf.lat , self.conf.lon
380         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 ) ) :
381             if ap[3] > 1 :
382                 self.plot( pixmap , ( ap[4]/ap[3] , ap[5]/ap[3] ) , "blue" )
383         db.close()
384
385         pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
386
387 class mapWidget ( gtk.EventBox ) :
388
389     def __init__ ( self , config ) :
390         gtk.EventBox.__init__( self )
391         self.mapwidget = simpleMapWidget( config )
392         self.add( self.mapwidget )
393
394         self.click_x , self.click_y = None , None
395         self.set_events( gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK )
396         self.connect_object('button_press_event', self.press_event, self.mapwidget)
397         self.connect_object('button_release_event', self.release_event, self.mapwidget)
398
399     def press_event ( self , widget , event ) :
400         self.click_x , self.click_y = event.x , event.y
401
402     def release_event ( self , widget, event ) :
403         # NOTE : we use origin-current for deltas because the map center moves in the opposite direction
404         delta_x = int( self.click_x - event.x )
405         delta_y = int( self.click_y - event.y )
406         widget.do_change_refpix(delta_x, delta_y)
407         self.click_x , self.click_y = None , None
408
409 if __name__ == "__main__" :
410
411     class StaticConfiguration :
412
413         def __init__ ( self , type=None ) :
414             self._type = type
415
416             self.homedir , self.dbname = None , None
417             self.mapsdir , self.mapclass = os.path.join( os.environ['HOME'] , "MyDocs/.maps" ) , "OpenStreetMap I"
418
419             self.store_log , self.use_mapper , self.store_gps = None , None , None
420
421             self.lat , self.lon = 40.416 , -3.683
422             self.zoom = 15
423
424     def on_key_press ( widget, event , map ) :
425         if event.keyval == gtk.keysyms.Up :
426             map.do_change_reftile(0,-1)
427         elif event.keyval == gtk.keysyms.Down :
428             map.do_change_reftile( 0 , +1 )
429         elif event.keyval == gtk.keysyms.Right :
430             map.do_change_reftile( +1 , 0 )
431         elif event.keyval == gtk.keysyms.Left :
432             map.do_change_reftile( -1 , 0 )
433         elif event.keyval == 49 :
434             map.do_change_zoomlevel( -1 )
435         elif event.keyval == 50 :
436             map.do_change_zoomlevel( +1 )
437         else :
438             print "UNKNOWN",event.keyval
439
440     config = StaticConfiguration()
441     mapwidget = mapWidget( config )
442     window = gtk.Window()
443     window.connect("destroy", gtk.main_quit )
444     window.connect("key-press-event", on_key_press, mapwidget.mapwidget )
445     window.add( mapwidget )
446     window.show_all()
447     gtk.main()
448