BUGFIX : move drawed points with the map
[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     # 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]
54
55     def reload ( self , config ) :
56         self.tileloader.set_params( config )
57         for i in range( self.size[0] * self.size[1] ) :
58             self.fill[i] = False
59         self.loadtiles()
60
61     def loadtiles ( self ) :
62
63         for x in range(self.size[0]) :
64             for y in range(self.size[1]) :
65
66               if not self.fill[ self.index(x,y) ] :
67                 pixbuf = self.tileloader.get_tile( (x-self.center[0],y-self.center[1]) )
68                 if pixbuf :
69                     self.fill[ self.index(x,y) ] = True
70                 else :
71                     pixbuf = self.tileloader.emptytile()
72
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 )
76
77     
78     def do_change_refpix ( self , dx , dy ) :
79         dx , dy = self.tileloader.do_change_refpix( dx , dy )
80         if dx or dy :
81             self.do_change_reftile( dx , dy )
82
83     def do_change_reftile( self , dx , dy ) :
84         self.tileloader.do_change_reftile( dx , dy )
85
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] )
89
90         # width , source , destination
91         x_vals = [ pixsize[0] , 0 , 0 ]
92         y_vals = [ pixsize[1] , 0 , 0 ]
93
94         if dx :
95             x_vals[0] -= abs(dx) * self.tileloader.tilesize
96             x_vals[cmp(dx,0)] = abs(dx) * self.tileloader.tilesize
97             if dx > 0 :
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
100             if dx < 0 :
101               for x in range(1,1-dx) :
102                 for y in range(self.size[1]) : self.fill[ self.index(x,y) ] = False
103         if dy :
104             y_vals[0] -= abs(dy) * self.tileloader.tilesize
105             y_vals[cmp(dy,0)] = abs(dy) * self.tileloader.tilesize
106             if dy > 0 :
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
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,y) ] = False
112
113         self.draw_pixbuf( None , pixbuf , x_vals[1] , y_vals[1] , x_vals[-1] , y_vals[-1] , x_vals[0] , y_vals[0] )
114         self.loadtiles()
115
116
117 class tile_loader :
118
119     def __init__ ( self , conf ) :
120         self.tilesize = 256
121         self.rootbase = os.path.join( conf.mapsdir , conf.mapclass )
122         self.set_params( conf )
123
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
128
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
135         return tileshift
136
137     def do_change_reftile( self , dx , dy ) :
138         self.__reftile[0] += dx
139         self.__reftile[1] += dy
140
141     def get_reftile ( self ) :
142         return self.__reftile
143
144     def get_refpix ( self ) :
145         return self.__refpix
146
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 )
153
154     def lon2tilex ( self , lon , zoom ) :
155         return math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
156
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 )
160
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
166
167     def get_tile ( self , tile ) :
168         file = self.tilepath( self.__reftile[0] + tile[0] , self.__reftile[1] + tile[1] )
169         try :
170             os.stat(file)
171             return gtk.gdk.pixbuf_new_from_file( file )
172         except :
173             try :
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() )
179                     fd.close()
180                     # FIXME : can this actually produce a gobject.GError exception ?
181                     return gtk.gdk.pixbuf_new_from_file( file )
182             except :
183                 pass
184
185         return None
186
187     def tilepath( self , tilex , tiley ) :
188       return "%s/%s/%s.png" % ( self.rootdir , tilex , tiley )
189
190     def emptytile( self ) :
191         pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize )
192         pixbuf.fill( 0x00000000 )
193         return pixbuf
194
195
196 class AbstractmapWidget :
197
198   def __init__ ( self , config , map_size ) :
199
200     self.conf = config
201
202     # Maximum width should be 800, but actually gets reduced
203     self.win_x , self.win_y = map_size
204     self.tile_size = 256
205
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 )
208
209   def recenter ( self , latlon ) :
210
211         center = self.gps2pix( latlon , self.center() )
212         pixel = self.gps2pix( (self.conf.lat,self.conf.lon) , self.center() )
213
214         distance = math.sqrt( (pixel[0]-center[0])**2 + (pixel[1]-center[1])**2 )
215
216         # FIXME : instead of hardcoded, should depend on the actual display size
217         if distance > 150 :
218             self.conf.set_latlon( latlon )
219
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 )
222
223             self.composeMap()
224
225   def tilex2lon ( self , ( tilex , pixx ) , zoom ) :
226         tilex = float(tilex)
227         pixx = float(pixx)
228         return ( tilex + pixx/self.tile_size ) / 2.0 ** zoom * 360.0 - 180.0
229
230   def tiley2lat ( self , ( tiley , pixy ) , zoom ) :
231         tiley = float(tiley)
232         pixy = float(pixy)
233         tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tile_size ) / 2.0 ** zoom )
234         return math.degrees( math.atan( math.sinh( tiley ) ) )
235
236   def SetZoom( self , zoom ) :
237         self.hide()
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 )
243         self.composeMap()
244         self.show()
245
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] )
249
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] )
254
255   def gps2pix ( self , ( lat , lon ) , ( center_x , center_y ) ) :
256
257     x_pos = self.lon2tilex( lon , self.conf.zoom )
258     y_pos = self.lat2tiley( lat , self.conf.zoom )
259
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]
262
263     return dest_x , dest_y
264
265   def tilename ( self , x , y , zoom ) :
266     file = self.tile2file( self.reftile_x + x , self.reftile_y + y , zoom )
267     try :
268       os.stat(file)
269     except :
270     #  if mapDownload :
271       if False :
272         try :
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" :
276               return None
277           fd = open( file , 'w' )
278           fd.write( response.read() )
279           fd.close()
280         except :
281           return None
282       else :
283         return None
284     return file
285
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 ) :
289       os.mkdir(rootdir)
290     rootsubdir = "%s/%s" % ( rootdir , tilex )
291     if not os.path.isdir( rootsubdir ) :
292       os.mkdir(rootsubdir)
293     return "%s/%s.png" % ( rootsubdir , tiley )
294
295
296 class simpleMapWidget ( AbstractmapWidget , gtk.Image ) :
297
298     def __init__ ( self , config , map_size=(800,480) ) :
299         AbstractmapWidget.__init__( self , config , map_size )
300
301         gtk.Image.__init__(self)
302
303         self._bg = background_map( map_size , tile_loader( config ) )
304         self.config = config
305
306         self.update_background()
307     
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 )
313         self.draw_paths(p)
314         self.set_from_pixbuf(p)
315     
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()
320
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()
325
326     def do_change_zoomlevel ( self , dz ) :
327         self.config.zoom += dz
328         self._bg.reload( self.config )
329         self.update_background()
330
331     def composeMap( self ) :
332         center_x , center_y = self.center()
333
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 )
339                 if file is None :
340                     pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
341                     pixbuf.fill( 0x00000000 )
342                 else :
343                     try :
344                         pixbuf = gtk.gdk.pixbuf_new_from_file( file )
345                     except gobject.GError , ex :
346                         print "Corrupted file %s" % ( file )
347                         os.unlink( 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 )
350                         try :
351                             pixbuf = gtk.gdk.pixbuf_new_from_file( file )
352                         except :
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 )
355
356                 dest_x = self.tile_size * i + center_x
357                 dest_y = self.tile_size * j + center_y
358
359                 init_x = 0
360                 size_x = self.tile_size
361                 if dest_x < 0 :
362                    init_x = abs(dest_x)
363                    size_x = self.tile_size + dest_x
364                    dest_x = 0
365                 if dest_x + self.tile_size > self.win_x :
366                    size_x = self.win_x - dest_x
367     
368                 init_y = 0
369                 size_y = self.tile_size
370                 if dest_y < 0 :
371                    init_y = abs(dest_y)
372                    size_y = self.tile_size + dest_y
373                    dest_y = 0
374                 if dest_y + self.tile_size > self.win_y :
375                    size_y = self.win_y - dest_y
376
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 )
379                 del(pixbuf)
380
381     def center( self ) :
382
383         center_x , center_y = self.win_x / 2 , self.win_y / 2
384
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
388
389         return center_x , center_y
390
391     def plot( self , pixmap , coords , colorname , radius=3 ) :
392
393         center_x , center_y = self.center()
394
395         gc = pixmap.new_gc()
396         gc.foreground = pixmap.get_colormap().alloc_color( colorname )
397
398         dest_x , dest_y = self.gps2pix( coords , ( center_x , center_y ) )
399         pixmap.draw_rectangle(gc, True , dest_x , dest_y , radius , radius )
400
401     def draw_paths( self , pixbuf ) :
402
403         pixmap,mask = pixbuf.render_pixmap_and_mask()
404
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" )
411         fd.close()
412
413         pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
414
415     def plot_APs( self , pixbuf ) :
416
417         pixmap,mask = pixbuf.render_pixmap_and_mask()
418
419         db = wifimap.db.database( os.path.join( self.conf.homedir , self.conf.dbname ) )
420         db.open()
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 ) ) :
424             if ap[3] > 1 :
425                 self.plot( pixmap , ( ap[4]/ap[3] , ap[5]/ap[3] ) , "blue" )
426         db.close()
427
428         pixbuf.get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
429
430 class mapWidget ( gtk.EventBox ) :
431
432     def __init__ ( self , config ) :
433         gtk.EventBox.__init__( self )
434         self.mapwidget = simpleMapWidget( config )
435         self.add( self.mapwidget )
436
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)
441
442     def press_event ( self , widget , event ) :
443         self.click_x , self.click_y = event.x , event.y
444
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
451
452 if __name__ == "__main__" :
453
454     class StaticConfiguration :
455
456         def __init__ ( self , type=None ) :
457             self._type = type
458
459             self.homedir , self.dbname = None , None
460             self.mapsdir , self.mapclass = os.path.join( os.environ['HOME'] , "MyDocs/.maps" ) , "OpenStreetMap I"
461
462             self.store_log , self.use_mapper , self.store_gps = None , None , None
463
464             self.lat , self.lon = 40.416 , -3.683
465             self.zoom = 15
466
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 )
480         else :
481             print "UNKNOWN",event.keyval
482
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 )
489     window.show_all()
490     gtk.main()
491