#!/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 <http://www.gnu.org/licenses/>.
'''

#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
