99a5eae723f4aaf85be0b05e705ffd45003c2b7c
[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
13     def __init__ ( self , map_size , tileloader ) :
14         bordersize = 1
15
16         self.tileloader = tileloader
17
18         # Values for minimun fit without border
19         center = map( lambda x :     int( math.ceil( x / float(tileloader.tilesize) ) / 2 )     , map_size )
20         size = map( lambda x : 2 * x + 1 , center )
21
22         self.center = map( lambda x : x + bordersize , center )
23         self.size = map( lambda x : x + 2 * bordersize , size )
24         pixsize = map( lambda x : x * tileloader.tilesize , self.size )
25
26         # FIXME : seems that reproducing the previous behaviour requires an extra subtraction of 128 to vpor[1]
27         #         when moving to non-integer viewports, the shift turns and adition, and changes to vpor[0]
28         self.vport = [ bordersize * tileloader.tilesize + tileloader.refpix[0] , bordersize * tileloader.tilesize + tileloader.refpix[1] ]
29
30         gtk.gdk.Pixmap.__init__( self , None , pixsize[0] , pixsize[1] , 24 )
31
32         self.fill = map( lambda x : False , range( self.size[0] * self.size[1] ) )
33
34         self.loadtiles()
35
36     def index ( self , x , y ) :
37         return x + y * self.size[0]
38
39     def loadtiles ( self ) :
40
41         for x in range(self.size[0]) :
42             for y in range(self.size[1]) :
43
44               if not self.fill[ self.index(x,y) ] :
45                 pixbuf = self.tileloader.get_tile( (x-self.center[0],y-self.center[1]) )
46                 if pixbuf :
47                     self.fill[ self.index(x,y) ] = True
48                 else :
49                     pixbuf = self.tileloader.emptytile()
50
51                 dest_x = self.tileloader.tilesize * x
52                 dest_y = self.tileloader.tilesize * y
53                 self.draw_pixbuf( None , pixbuf , 0 , 0 , dest_x , dest_y )
54
55     
56     def do_change_reftile( self , dx , dy ) :
57         self.tileloader.reftile[0] += dx
58         self.tileloader.reftile[1] += dy
59
60         pixsize = self.get_size()
61         pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, pixsize[0] , pixsize[1] )
62         pixbuf.get_from_drawable( self , self.get_colormap() , 0 , 0 , 0 , 0 , pixsize[0] , pixsize[1] )
63
64         # width , source , destination
65         x_vals = [ pixsize[0] , 0 , 0 ]
66         y_vals = [ pixsize[1] , 0 , 0 ]
67
68         if dx :
69             x_vals[0] -= abs(dx) * self.tileloader.tilesize
70             x_vals[cmp(dx,0)] = abs(dx) * self.tileloader.tilesize
71             if dx > 0 :
72               for x in range(1,1+dx) :
73                 for y in range(self.size[1]) : self.fill[ self.index(self.size[0]-x,y) ] = False
74             if dx < 0 :
75               for x in range(1,1-dx) :
76                 for y in range(self.size[1]) : self.fill[ self.index(x,y) ] = False
77         if dy :
78             y_vals[0] -= abs(dy) * self.tileloader.tilesize
79             y_vals[cmp(dy,0)] = abs(dy) * self.tileloader.tilesize
80             if dy > 0 :
81               for y in range(1,1+dy) :
82                 for x in range(self.size[0]) : self.fill[ self.index(x,self.size[1]-y) ] = False
83             if dy < 0 :
84               for y in range(1,1-dy) :
85                 for x in range(self.size[0]) : self.fill[ self.index(x,y) ] = False
86
87         self.draw_pixbuf( None , pixbuf , x_vals[1] , y_vals[1] , x_vals[-1] , y_vals[-1] , x_vals[0] , y_vals[0] )
88         self.loadtiles()
89
90
91 class tile_loader :
92
93     def __init__ ( self , conf ) :
94         self.tilesize = 256
95         self.rootdir = "%s/%s/%s" % ( conf.mapsdir , conf.mapclass , conf.zoom )
96         self.reftile , self.refpix = self.get_reference( conf )
97
98     def get_reference ( self , conf ) :
99         tilex = self.lon2tilex( conf.lon , conf.zoom )
100         tiley = self.lat2tiley( conf.lat , conf.zoom )
101         tile = tilex[1] , tiley[1] 
102         pix = tilex[0] , tiley[0] 
103         return map( int , tile ) , map( lambda x : int( self.tilesize * x ) , pix )
104
105     def lon2tilex ( self , lon , zoom ) :
106         return math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
107
108     def lat2tiley ( self , lat , zoom ) :
109         lat = lat * math.pi / 180
110         return math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
111
112     def get_tile ( self , tile ) :
113         file = self.tilepath( self.reftile[0] + tile[0] , self.reftile[1] + tile[1] )
114         try :
115             os.stat(file)
116             return gtk.gdk.pixbuf_new_from_file( file )
117         except :
118             try :
119                 # useful members : response.code, response.headers
120                 response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
121                 if response.geturl() != "http://tile.openstreetmap.org/11/0/0.png" :
122                     fd = open( file , 'w' )
123                     fd.write( response.read() )
124                     fd.close()
125                     # FIXME : can this actually produce a gobject.GError exception ?
126                     return gtk.gdk.pixbuf_new_from_file( file )
127             except :
128                 pass
129
130         return None
131
132     def tilepath( self , tilex , tiley ) :
133       return "%s/%s/%s.png" % ( self.rootdir , tilex , tiley )
134
135     def emptytile( self ) :
136         pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tilesize, self.tilesize )
137         pixbuf.fill( 0x00000000 )
138         return pixbuf
139
140
141 class AbstractmapWidget :
142
143   def __init__ ( self , config , map_size ) :
144
145     self.conf = config
146
147     # Maximum width should be 800, but actually gets reduced
148     self.win_x , self.win_y = map_size
149     self.tile_size = 256
150
151     self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
152     self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
153
154   def recenter ( self , latlon ) :
155
156         center = self.gps2pix( latlon , self.center() )
157         pixel = self.gps2pix( (self.conf.lat,self.conf.lon) , self.center() )
158
159         distance = math.sqrt( (pixel[0]-center[0])**2 + (pixel[1]-center[1])**2 )
160
161         # FIXME : instead of hardcoded, should depend on the actual display size
162         if distance > 150 :
163             self.conf.set_latlon( latlon )
164
165             self.reftile_x , self.refpix_x = self.lon2tilex( self.conf.lon , self.conf.zoom )
166             self.reftile_y , self.refpix_y = self.lat2tiley( self.conf.lat , self.conf.zoom )
167
168             self.composeMap()
169
170   def tilex2lon ( self , ( tilex , pixx ) , zoom ) :
171         tilex = float(tilex)
172         pixx = float(pixx)
173         return ( tilex + pixx/self.tile_size ) / 2.0 ** zoom * 360.0 - 180.0
174
175   def tiley2lat ( self , ( tiley , pixy ) , zoom ) :
176         tiley = float(tiley)
177         pixy = float(pixy)
178         tiley = math.pi * ( 1 - 2 * ( tiley + pixy/self.tile_size ) / 2.0 ** zoom )
179         return math.degrees( math.atan( math.sinh( tiley ) ) )
180
181   def SetZoom( self , zoom ) :
182         self.hide()
183         lat = self.tiley2lat( ( self.reftile_y , self.refpix_y ) , self.conf.zoom )
184         lon = self.tilex2lon( ( self.reftile_x , self.refpix_x ) , self.conf.zoom )
185         self.reftile_x , self.refpix_x = self.lon2tilex( lon , zoom )
186         self.reftile_y , self.refpix_y = self.lat2tiley( lat , zoom )
187         self.conf.set_zoom( zoom )
188         self.composeMap()
189         self.show()
190
191   def lon2tilex ( self , lon , zoom ) :
192     number = math.modf( ( lon + 180 ) / 360 * 2 ** zoom )
193     return int( number[1] ) , int( self.tile_size * number[0] )
194
195   def lat2tiley ( self , lat , zoom ) :
196     lat = lat * math.pi / 180
197     number = math.modf( ( 1 - math.log( math.tan( lat ) + 1 / math.cos( lat ) ) / math.pi ) / 2 * 2 ** zoom )
198     return int( number[1] ) , int( self.tile_size * number[0] )
199
200   def gps2pix ( self , ( lat , lon ) , ( center_x , center_y ) ) :
201
202     x_pos = self.lon2tilex( lon , self.conf.zoom )
203     y_pos = self.lat2tiley( lat , self.conf.zoom )
204
205     dest_x = self.tile_size * ( x_pos[0] - self.reftile_x ) + center_x + x_pos[1]
206     dest_y = self.tile_size * ( y_pos[0] - self.reftile_y ) + center_y + y_pos[1]
207
208     return dest_x , dest_y
209
210   def tilename ( self , x , y , zoom ) :
211     file = self.tile2file( self.reftile_x + x , self.reftile_y + y , zoom )
212     try :
213       os.stat(file)
214     except :
215     #  if mapDownload :
216       if False :
217         try :
218           # useful members : response.code, response.headers
219           response = urllib2.urlopen( "http://tile.openstreetmap.org/%s/%s/%s.png" % ( zoom , x , y ) )
220           if response.geturl() == "http://tile.openstreetmap.org/11/0/0.png" :
221               return None
222           fd = open( file , 'w' )
223           fd.write( response.read() )
224           fd.close()
225         except :
226           return None
227       else :
228         return None
229     return file
230
231   def tile2file( self , tilex , tiley , zoom ) :
232     rootdir = "%s/%s/%s" % ( self.conf.mapsdir , self.conf.mapclass , zoom )
233     if not os.path.isdir( rootdir ) :
234       os.mkdir(rootdir)
235     rootsubdir = "%s/%s" % ( rootdir , tilex )
236     if not os.path.isdir( rootsubdir ) :
237       os.mkdir(rootsubdir)
238     return "%s/%s.png" % ( rootsubdir , tiley )
239
240 class interactiveMapWidget :
241
242   def Shift( self , dx , dy ) :
243
244 #    tile_x , tile_y = ( self.refpix_x - dx ) / self.tile_size , ( self.refpix_y - dy ) / self.tile_size
245 #    self.reftile_x += tile_x
246 #    self.reftile_y += tile_y
247
248 #    self.refpix_x -= dx + self.tile_size * tile_x
249 #    self.refpix_y -= dy + self.tile_size * tile_y
250
251         self.mapwidget._bg.vport[0] += dx
252         self.mapwidget._bg.vport[1] += dy
253         self.mapwidget.update_background()
254
255   def Up( self ) :
256         self.mapwidget._bg.do_change_reftile( 0 , -1 )
257         self.mapwidget.update_background()
258
259   def Down( self ) :
260         self.mapwidget._bg.do_change_reftile( 0 , +1 )
261         self.mapwidget.update_background()
262
263   def Right( self ) :
264         self.mapwidget._bg.do_change_reftile( +1 , 0 )
265         self.mapwidget.update_background()
266
267   def Left( self ) :
268         self.mapwidget._bg.do_change_reftile( -1 , 0 )
269         self.mapwidget.update_background()
270
271
272 class simpleMapWidget ( AbstractmapWidget , gtk.Image ) :
273
274     def __init__ ( self , config , map_size=(800,480) ) :
275         AbstractmapWidget.__init__( self , config , map_size )
276
277         gtk.Image.__init__(self)
278
279         self._bg = background_map( map_size , tile_loader( config ) )
280
281         self.update_background()
282     
283     def update_background( self ) :
284         p = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.win_x , self.win_y )
285         p.get_from_drawable( self._bg , self._bg.get_colormap() , self._bg.vport[0] , self._bg.vport[1] , 0 , 0 , self.win_x , self.win_y )
286         self.set_from_pixbuf(p)
287     
288     def composeMap( self ) :
289         center_x , center_y = self.center()
290
291         # Ranges should be long enough as to fill the screen
292         # Maybe they should be decided based on self.win_x, self.win_y
293         for i in range(-3,4) :
294             for j in range(-3,4) :
295                 file = self.tilename( i , j , self.conf.zoom )
296                 if file is None :
297                     pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
298                     pixbuf.fill( 0x00000000 )
299                 else :
300                     try :
301                         pixbuf = gtk.gdk.pixbuf_new_from_file( file )
302                     except gobject.GError , ex :
303                         print "Corrupted file %s" % ( file )
304                         os.unlink( file )
305                         #file = self.tilename( self.reftile_x + i , self.reftile_y + j , self.conf.zoom )
306                         file = self.tilename( i , j , self.conf.zoom )
307                         try :
308                             pixbuf = gtk.gdk.pixbuf_new_from_file( file )
309                         except :
310                             print "Total failure for tile for %s,%s" % ( self.reftile_x + i , self.reftile_y + j )
311                             pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.tile_size, self.tile_size )
312
313                 dest_x = self.tile_size * i + center_x
314                 dest_y = self.tile_size * j + center_y
315
316                 init_x = 0
317                 size_x = self.tile_size
318                 if dest_x < 0 :
319                    init_x = abs(dest_x)
320                    size_x = self.tile_size + dest_x
321                    dest_x = 0
322                 if dest_x + self.tile_size > self.win_x :
323                    size_x = self.win_x - dest_x
324     
325                 init_y = 0
326                 size_y = self.tile_size
327                 if dest_y < 0 :
328                    init_y = abs(dest_y)
329                    size_y = self.tile_size + dest_y
330                    dest_y = 0
331                 if dest_y + self.tile_size > self.win_y :
332                    size_y = self.win_y - dest_y
333
334                 if ( size_x > 0 and size_y > 0 ) and ( init_x < self.tile_size and init_y < self.tile_size ) :
335                     pixbuf.copy_area( init_x, init_y, size_x, size_y, self.get_pixbuf(), dest_x , dest_y )
336                 del(pixbuf)
337
338 #        self.draw_paths()
339 #        self.plot_APs()
340
341     def center( self ) :
342
343         center_x , center_y = self.win_x / 2 , self.win_y / 2
344
345         # To get the central pixel in the window center, we must shift to the tile origin
346         center_x -= self.refpix_x
347         center_y -= self.refpix_y
348
349         return center_x , center_y
350
351     def plot( self , pixmap , coords , colorname , radius=3 ) :
352
353         center_x , center_y = self.center()
354
355         gc = pixmap.new_gc()
356         gc.foreground = pixmap.get_colormap().alloc_color( colorname )
357
358         dest_x , dest_y = self.gps2pix( coords , ( center_x , center_y ) )
359         pixmap.draw_rectangle(gc, True , dest_x , dest_y , radius , radius )
360
361     def draw_paths( self ) :
362
363         pixmap,mask = self.get_pixbuf().render_pixmap_and_mask()
364
365         filename = "data/wiscan_gui.info.old"
366         fd = open( filename )
367         for line in fd.readlines() :
368             values = line.split()
369             if values[1] == "FIX" :
370                 self.plot( pixmap , ( float(values[5]) , float(values[6]) ) , "red" )
371         fd.close()
372
373         self.get_pixbuf().get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
374
375     def plot_APs( self ) :
376
377         pixmap,mask = self.get_pixbuf().render_pixmap_and_mask()
378
379         db = wifimap.db.database( os.path.join( self.conf.homedir , self.conf.dbname ) )
380         db.open()
381         # NOTE : Intervals for query are just educated guesses to fit in window
382         lat , lon = self.conf.lat , self.conf.lon
383         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 ) ) :
384             if ap[3] > 1 :
385                 self.plot( pixmap , ( ap[4]/ap[3] , ap[5]/ap[3] ) , "blue" )
386         db.close()
387
388         self.get_pixbuf().get_from_drawable( pixmap , pixmap.get_colormap() , 0, 0 , 0 , 0 , self.win_x, self.win_y )
389
390 class mapWidget ( gtk.EventBox , interactiveMapWidget ) :
391
392     def __init__ ( self , config ) :
393         gtk.EventBox.__init__( self )
394         self.mapwidget = simpleMapWidget( config )
395         self.add( self.mapwidget )
396
397         self.click_x , self.click_y = None , None
398         self.set_events( gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK )
399         self.connect_object('button_press_event', self.press_event, self.mapwidget)
400         self.connect_object('button_release_event', self.release_event, self)
401
402     def press_event ( self , widget , event ) :
403         self.click_x , self.click_y = event.x , event.y
404
405     def release_event ( self , widget, event ) :
406         delta_x = int( self.click_x - event.x )
407         delta_y = int( self.click_y - event.y )
408         widget.Shift(delta_x, delta_y)
409         self.click_x , self.click_y = None , None
410
411 if __name__ == "__main__" :
412
413     class StaticConfiguration :
414
415         def __init__ ( self , type=None ) :
416             self._type = type
417
418             self.homedir , self.dbname = None , None
419             self.mapsdir , self.mapclass = "/boot/home/localuser/Maps" , "OpenStreetMap I"
420
421             self.store_log , self.use_mapper , self.store_gps = None , None , None
422
423             self.lat , self.lon = 40.416 , -3.683
424             self.zoom = 15
425
426     def on_key_press ( widget, event , map ) :
427         if event.keyval == gtk.keysyms.Up :
428             map.Up()
429         elif event.keyval == gtk.keysyms.Down :
430             map.Down()
431         elif event.keyval == gtk.keysyms.Right :
432             map.Right()
433         elif event.keyval == gtk.keysyms.Left :
434             map.Left()
435         else :
436             print "UNKNOWN",event.keyval
437
438     config = StaticConfiguration()
439     mapwidget = mapWidget( config )
440     window = gtk.Window()
441     window.connect("destroy", gtk.main_quit )
442     window.connect("key-press-event", on_key_press, mapwidget )
443     window.add( mapwidget )
444     window.show_all()
445     gtk.main()
446