#!/usr/bin/python #coding=utf8 ''' This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' #TODO: restructure tile loading #TODO: add wikipedia geotagging #TODO: add dynamic preloading #TODO: add gps connection import os import sys from e_dbus import DBusEcoreMainLoop import evas import evas.decorators import edje import edje.decorators import ecore import ecore.evas from dbus import SystemBus, Interface import time import math from urllib2 import build_opener from freesmartphone import DBUS_NAME, DIN_LOCATION from base import dedbusmap def err(*data): print 'err', data class PylgrimMap(object): def __init__(self): self.opener = build_opener() def download(self, x, y, zoom): filename = "%d/%d/%d.png"%(zoom, x, y) print 'download', filename try: dirname = "%d/%d"%(zoom, x) if not os.path.exists(dirname): os.makedirs(dirname) localFile = open(filename, 'w') webFile = self.opener.open("http://a.tile.openstreetmap.org/%d/%d/%d.png"%(zoom, x, y)) localFile.write(webFile.read()) webFile.close() localFile.close() except Exception, e: print 'download error', e if os.path.exists(filename): os.unlink(filename) class PylgrimView(edje.Edje, PylgrimMap): class Tile(evas.Image): def __init__(self, canvas): evas.Image.__init__(self, canvas) self.pass_events = True self.show() #we need this to store the original position while the zoom animations self.position = (0, 0) def set_position(self, x, y): self.position = (x, y) self.move(x, y) class Mark(evas.Image): def __init__(self, canvas): evas.Image.__init__(self, canvas) self.pass_events = True self.show() #we need this to store the original position while the zoom animations self.position = (0, 0) def set_position(self, x, y): self.position = (x, y) self.move(x, y) def __init__(self, evas_canvas, filename, latitude, longitude, zoom=10, offline=False, use_overlay=True): PylgrimMap.__init__(self) self.evas_canvas = evas_canvas self.offline = offline self.use_overlay = use_overlay edje.Edje.__init__(self, self.evas_canvas.evas_obj.evas, file=filename, group="pylgrim") self.size = self.evas_canvas.evas_obj.evas.size self.on_key_down_add(self.on_key_down) self.on_key_up_add(self.on_key_up) self.focus = True self.evas_canvas.evas_obj.data["pylgrim"] = self self.show() #mouse position self.x_pos, self.y_pos = (0, 0) #global variable for zooming self.zoom_step = 0.0 #global list for tiles to download self.tiles_to_download = [] self.tiles_to_download_total = 0 self.tiles_to_preload = [] #initial latitude, longitude, zoom self.latitude = latitude self.longitude = longitude self.x = 0 self.y = 0 self.zoom = zoom self.offset_x = 0 self.offset_y = 0 self.icons = [] self.overlay = edje.Edje(self.evas_canvas.evas_obj.evas, file=filename, group='overlay') self.overlay.size = self.evas_canvas.evas_obj.evas.size self.overlay.layer = 2 self.evas_canvas.evas_obj.data["overlay"] = self.overlay self.overlay.show() if self.use_overlay: self.progress_bg = evas.Rectangle(self.evas_canvas.evas_obj.evas) self.progress_bg.geometry = 0, 0, 0, 0 self.progress_bg.color = 255, 255, 255, 255 self.progress_bg.layer = 3 self.progress_bg.show() self.progress = evas.Rectangle(self.evas_canvas.evas_obj.evas) self.progress.geometry = 0, 0, 0, 0 self.progress.color = 255, 0, 0, 255 self.progress.layer = 4 self.progress.show() ''' self.menu = edje.Edje(self.evas_canvas.evas_obj.evas, file=filename, group='menu') self.menu.size = self.evas_canvas.evas_obj.evas.size self.menu.layer = 4 self.evas_canvas.evas_obj.data["menu"] = self.menu self.menu.show() ''' #calculate size of tile raster self.border_x = int(math.ceil(self.size[0]/256.0)) self.border_y = int(math.ceil(self.size[1]/256.0)) self.mouse_down = False self.animate = False self.set_current_tile(self.latitude, self.longitude, zoom) ''' self.marker = PylgrimView.Mark(self.evas_canvas.evas_obj.evas) self.marker.file_set("blue-dot.png") self.marker.latitude = 49.073866 self.marker.longitude = 8.184814 self.marker.size_set(32, 32) self.marker.fill_set(0, 0, 32, 32) self.marker.x = (self.marker.longitude+180)/360 * 2**self.zoom self.marker.y = (1-math.log(math.tan(self.marker.latitude*math.pi/180) + 1/math.cos(self.marker.latitude*math.pi/180))/math.pi)/2 * 2**self.zoom self.marker.offset_x, self.marker.offset_y = int((self.marker.x-int(self.marker.x))*256), int((self.marker.y-int(self.marker.y))*256) self.marker.set_position(self.size[0]/2-16+256*(int(self.marker.x)-int(self.x))+self.marker.offset_x-self.offset_x, self.size[1]/2-32+256*(int(self.marker.y)-int(self.y))+self.marker.offset_y-self.offset_y) self.marker.layer = 1 self.marker.show() ''' ecore.timer_add(3, self.init_dbus) def init_dbus(self): print 'PylgrimView init_dbus' try: gps_obj = SystemBus(mainloop=DBusEcoreMainLoop()).get_object(DBUS_NAME, #'/org/mobile/GoogleLocation', '/org/mobile/GpsLocation', ) gps_obj.connect_to_signal("position", self.position, dbus_interface=DIN_LOCATION) self.gps_interface = Interface(gps_obj, DIN_LOCATION) self.gps_interface.GetPosition(reply_handler=self.position, error_handler=err, ) return False except Exception, e: print 'PylgrimView', e return True def position(self, content): content = dedbusmap(content) print 'position', content fix = int(content.get('fix', 0)) if fix: latitude = float(content.get('latitude', self.latitude)) longitude = float(content.get('longitude', self.longitude)) print 'position', latitude, longitude if not self.animate: self.set_current_tile(latitude, longitude, self.zoom) def on_key_down(self, obj, event): if event.keyname in ("F6", "f"): self.evas_canvas.evas_obj.fullscreen = not self.evas_canvas.evas_obj.fullscreen elif event.keyname in ("Escape", "q"): ecore.main_loop_quit() elif event.keyname in ("F7", "plus") and not self.animate: ecore.timer_add(0.05, self.animate_zoom_in) elif event.keyname in ("F8", "minus") and not self.animate: ecore.timer_add(0.05, self.animate_zoom_out) elif event.keyname in ("Up", ) and not self.animate: delta_y = -10 for icon in self.icons: icon.set_position(icon.pos[0], icon.pos[1]-delta_y) self.current_pos = (self.current_pos[0], self.current_pos[1]-delta_y) elif event.keyname in ("Down", ) and not self.animate: delta_y = 10 for icon in self.icons: icon.set_position(icon.pos[0], icon.pos[1]-delta_y) self.current_pos = (self.current_pos[0], self.current_pos[1]-delta_y) elif event.keyname in ("Left", ) and not self.animate: delta_x = -10 for icon in self.icons: icon.set_position(icon.pos[0]-delta_x, icon.pos[1]) self.current_pos = (self.current_pos[0]-delta_x, self.current_pos[1]) elif event.keyname in ("Right", ) and not self.animate: delta_x = 10 for icon in self.icons: icon.set_position(icon.pos[0]-delta_x, icon.pos[1]) self.current_pos = (self.current_pos[0]-delta_x, self.current_pos[1]) else: print "key not recognized:", event.keyname def on_key_up(self, obj, event): if event.keyname in ("Up", "Down", "Left", "Right") and not self.animate: if abs(self.current_pos[0]) > self.size[0]/2 or abs(self.current_pos[1]) > self.size[1]/2: self.x = int(self.x) + (self.offset_x-self.current_pos[0])/256.0 self.y = int(self.y) + (self.offset_y-self.current_pos[1])/256.0 self.offset_x, self.offset_y = int((self.x-int(self.x))*256), int((self.y-int(self.y))*256) self.init_redraw() self.update_coordinates() #jump to coordinates def set_current_tile(self, latitude, longitude, zoom): #update shown coordinates everytime they change self.overlay.part_text_set("label", "latitude:%f longitude:%f zoom:%d"%(latitude, longitude, zoom)) x = (longitude+180)/360 * 2**zoom y = (1-math.log(math.tan(latitude*math.pi/180) + 1/math.cos(latitude*math.pi/180))/math.pi)/2 * 2**zoom offset_x, offset_y = int((x-int(x))*256), int((y-int(y))*256) #only redraw if x, y, zoom, offset_x or offset_y differ from before if int(x) != int(self.x) \ or int(y) != int(self.y) \ or zoom != self.zoom \ or offset_x != self.offset_x \ or offset_y != self.offset_y: self.zoom = zoom self.x = x self.y = y self.offset_x, self.offset_y = offset_x, offset_y self.init_redraw() def init_redraw(self): print "redraw" self.animate = True #reload icons list if its length differs from before eg. when size changes if len(self.icons) != (2*self.border_x+1)*(2*self.border_y+1): print "x:", self.border_x, "y:", self.border_y #clean up for icon in self.icons: icon.delete() self.icons = [] #fill for i in xrange((2*self.border_x+1)*(2*self.border_y+1)): self.icons.append(PylgrimView.Tile(self.evas_canvas.evas_obj.evas)) if not self.offline: #add all tiles that are not yet downloaded to a list for i in xrange(2*self.border_x+1): for j in xrange(2*self.border_y+1): if not os.path.exists("%d/%d/%d.png"%(self.zoom, self.x+i-self.border_x, self.y+j-self.border_y))\ and not (self.zoom, self.x+i-self.border_x, self.y+j-self.border_y) in self.tiles_to_download: self.tiles_to_download.append((self.zoom, self.x+i-self.border_x, self.y+j-self.border_y)) self.tiles_to_download_total = len(self.tiles_to_download) ''' #add additional tiles around the raster to a preload list for i in xrange(2*self.border_x+3): if i == 0 or i == 2*self.border_x+2: #if first or last row, download full row for j in xrange(2*self.border_y+3): if not os.path.exists("%d/%d/%d.png"%(self.zoom, self.x+i-self.border_x-1, self.y+j-self.border_y-1)): self.tiles_to_preload.append((self.zoom, self.x+i-self.border_x-1, self.y+j-self.border_y-1)) #lots TODO here #let preload more than one tile - maybe a preload_border_x/y variable #manage simultaneos proeloads #manage not preloading duplicates else #else download first and last tile ''' #if there are tiles to download, display progress bar if self.use_overlay and self.tiles_to_download_total > 0: self.progress_bg.geometry = 39, self.size[1]/2-1, self.size[0]-78, 22 self.progress.geometry = 40, self.size[1]/2, 1, 20 self.overlay.part_text_set("progress", "downloaded 0 of %d tiles"%self.tiles_to_download_total) ecore.timer_add(0.0, self.download_and_paint_current_tiles) def download_and_paint_current_tiles(self): if len(self.tiles_to_download) > 0: zoom, x, y = self.tiles_to_download.pop() if self.use_overlay: self.progress.geometry = 40, self.size[1]/2, (self.size[0]-80)*(self.tiles_to_download_total-len(self.tiles_to_download))/self.tiles_to_download_total, 20 self.overlay.part_text_set("progress", "downloaded %d of %d tiles"%(self.tiles_to_download_total-len(self.tiles_to_download), self.tiles_to_download_total)) self.download(x, y, zoom) return True #we get here if all tiles are downloaded for i in xrange(2*self.border_x+1): for j in xrange(2*self.border_y+1): #if some errors occur replace with placeholder filename = "%d/%d/%d.png"%(self.zoom, self.x+i-self.border_x, self.y+j-self.border_y) try: self.icons[(2*self.border_y+1)*i+j].file_set(filename) except Exception, e: print e if os.path.exists(filename): os.unlink(filename) self.icons[(2*self.border_y+1)*i+j].file_set("404.png") self.icons[(2*self.border_y+1)*i+j].set_position((i-self.border_x)*256+self.size[0]/2-self.offset_x, (j-self.border_y)*256+self.size[1]/2-self.offset_y) self.icons[(2*self.border_y+1)*i+j].size = 256, 256 self.icons[(2*self.border_y+1)*i+j].fill = 0, 0, 256, 256 self.current_pos = (0, 0) if self.use_overlay: self.overlay.part_text_set("progress", "") self.progress_bg.geometry = 0, 0, 0, 0 self.progress.geometry = 0, 0, 0, 0 self.animate = False return False def update_coordinates(self): x = int(self.x) + (self.offset_x-self.current_pos[0])/256.0 y = int(self.y) + (self.offset_y-self.current_pos[1])/256.0 self.longitude = (x*360)/2**self.zoom-180 n = math.pi*(1-2*y/2**self.zoom) self.latitude = 180/math.pi*math.atan(0.5*(math.exp(n)-math.exp(-n))) self.overlay.part_text_set("label", "latitude:%f longitude:%f zoom:%d"%(self.latitude, self.longitude, self.zoom)) def zoom_in(self, zoom): for icon in self.icons: x = (1+zoom)*(icon.position[0]-self.size[0]/2)+self.size[0]/2 y = (1+zoom)*(icon.position[1]-self.size[1]/2)+self.size[1]/2 icon.geometry = int(x), int(y), 256+int(256*zoom), 256+int(256*zoom) icon.fill = 0, 0, 256+int(256*zoom), 256+int(256*zoom) def zoom_out(self, zoom): for icon in self.icons: x = (1-zoom*0.5)*(icon.position[0]-self.size[0]/2)+self.size[0]/2 y = (1-zoom*0.5)*(icon.position[1]-self.size[1]/2)+self.size[1]/2 icon.geometry = int(x), int(y), 256-int(256*zoom*0.5), 256-int(256*zoom*0.5) icon.fill = 0, 0, 256-int(256*zoom*0.5), 256-int(256*zoom*0.5) def animate_zoom_in(self): if self.zoom < 18: self.animate = True if self.zoom_step < 1.0: self.zoom_in(self.zoom_step) self.zoom_step+=0.125 return True self.zoom_step = 0.0 self.set_current_tile(self.latitude, self.longitude, self.zoom+1) else: self.animate = False return False def animate_zoom_out(self): if self.zoom > 5: self.animate = True if self.zoom_step < 1.0: self.zoom_out(self.zoom_step) self.zoom_step+=0.125 return True self.zoom_step = 0.0 self.set_current_tile(self.latitude, self.longitude, self.zoom-1) else: self.animate = False return False @edje.decorators.signal_callback("mouse,down,1", "*") def on_mouse_down(self, emission, source): if not self.animate: if source in "plus": ecore.timer_add(0.05, self.animate_zoom_in) elif source in "minus": ecore.timer_add(0.05, self.animate_zoom_out) else: self.x_pos, self.y_pos = self.evas_canvas.evas_obj.evas.pointer_canvas_xy self.mouse_down = True @edje.decorators.signal_callback("mouse,up,1", "*") def on_mouse_up(self, emission, source): self.mouse_down = False if not self.animate: #redraw if moved further than one tile in each direction 'cause the preoload will only download one tile further than requested if abs(self.current_pos[0]) > 256 or abs(self.current_pos[1]) > 256: self.x = int(self.x) + (self.offset_x-self.current_pos[0])/256.0 self.y = int(self.y) + (self.offset_y-self.current_pos[1])/256.0 self.offset_x, self.offset_y = int((self.x-int(self.x))*256), int((self.y-int(self.y))*256) self.init_redraw() self.update_coordinates() if abs(self.current_pos[0]) > 0 or abs(self.current_pos[1]) > 0: #on mouse up + move: update current coordinates self.update_coordinates() @edje.decorators.signal_callback("mouse,move", "*") def on_mouse_move(self, emission, source): if self.mouse_down and not self.animate: x_pos, y_pos = self.evas_canvas.evas_obj.evas.pointer_canvas_xy delta_x = self.x_pos - x_pos delta_y = self.y_pos - y_pos self.x_pos, self.y_pos = x_pos, y_pos for icon in self.icons: icon.set_position(icon.pos[0]-delta_x, icon.pos[1]-delta_y) self.current_pos = (self.current_pos[0]-delta_x, self.current_pos[1]-delta_y) #self.marker.set_position(self.marker.pos[0]-delta_x, self.marker.pos[1]-delta_y) class PylgrimControl(PylgrimView): def __init__(self, evas_canvas, filename, latitude, longitude, zoom=10, offline=False, use_overlay=True): PylgrimView.__init__(self, evas_canvas, filename, latitude, longitude, zoom=10, offline=False, use_overlay=True) if __name__ == "__main__": WIDTH = 480 HEIGHT = 640 TITLE = "pylgrim" WM_NAME = "pylgrim" WM_CLASS = "swallow" from optparse import OptionParser class myOptionParser(OptionParser): def __init__(self, usage): OptionParser.__init__(self, usage) self.add_option("-e", "--engine", type="choice", choices=("x11", "x11-16"), default="x11-16", help=("which display engine to use (x11, x11-16), " "default=%default")) self.add_option("-n", "--no-fullscreen", action="store_true", help="do not launch in fullscreen") self.add_option("-o", "--offline", action="store_true", help="do not attempt to download tiles") self.add_option("-g", "--geometry", type="string", metavar="WxH", action="callback", callback=self.parse_geometry, default=(WIDTH, HEIGHT), help="use given window geometry") self.add_option("-f", "--fps", type="int", default=20, help="frames per second to use, default=%default") def parse_geometry(self, option, opt, value, parser): try: w, h = value.split("x") w = int(w) h = int(h) except Exception, e: raise optparse.OptionValueError("Invalid format for %s" % option) parser.values.geometry = (w, h) class EvasCanvas(object): def __init__(self, fullscreen, engine, size): if engine == "x11": f = ecore.evas.SoftwareX11 elif engine == "x11-16": if ecore.evas.engine_type_supported_get("software_x11_16"): f = ecore.evas.SoftwareX11_16 else: print "warning: x11-16 is not supported, fallback to x11" f = ecore.evas.SoftwareX11 self.evas_obj = f(w=size[0], h=size[1]) self.evas_obj.callback_delete_request = self.on_delete_request self.evas_obj.callback_resize = self.on_resize self.evas_obj.title = TITLE self.evas_obj.name_class = (WM_NAME, WM_CLASS) self.evas_obj.fullscreen = fullscreen self.evas_obj.size = size self.evas_obj.show() def on_resize(self, evas_obj): x, y, w, h = evas_obj.evas.viewport size = (w, h) for key in evas_obj.data.keys(): evas_obj.data[key].size = size #calculate size of tile raster evas_obj.data["pylgrim"].border_x = int(math.ceil(evas_obj.data["pylgrim"].size[0]/256.0)) evas_obj.data["pylgrim"].border_y = int(math.ceil(evas_obj.data["pylgrim"].size[1]/256.0)) evas_obj.data["pylgrim"].init_redraw() def on_delete_request(self, evas_obj): ecore.main_loop_quit() print 'start', __name__ options, args = myOptionParser(usage="usage: %prog [options]").parse_args() edje.frametime_set(1.0 / options.fps) evas_canvas = EvasCanvas( fullscreen=not options.no_fullscreen, engine=options.engine, size=options.geometry ) filename = os.path.splitext(sys.argv[0])[0] + ".edj" PylgrimControl(evas_canvas, filename, 49.009051, 8.402481, 13, options.offline, ) ecore.main_loop_begin() # vim:tw=0:nowrap