from math import pow, cos, pi, radians, degrees, atan, tan, sinh, log
from ipywidgets import Image, IntSlider
from ipyevents import Event
from ipycanvas import Canvas, MultiCanvas, hold_canvas
import asyncio
class Timer:
def __init__(self, timeout, callback):
self._timeout = timeout
self._callback = callback
self._task = asyncio.ensure_future(self._job())
async def _job(self):
await asyncio.sleep(self._timeout)
self._callback()
def cancel(self):
self._task.cancel()
def debounce(wait):
""" Decorator that will postpone a function's
execution until after `wait` seconds
have elapsed since the last time it was invoked. """
def decorator(fn):
timer = None
def debounced(*args, **kwargs):
nonlocal timer
def call_it():
fn(*args, **kwargs)
if timer is not None:
timer.cancel()
timer = Timer(wait, call_it)
return debounced
return decorator
def numTiles(z):
return pow(2, z)
def sec(x):
return 1 / cos(x)
def latlon2relativeXY(lat, lon):
x = (lon + 180) / 360
y = (1 - log(tan(radians(lat)) + sec(radians(lat))) / pi) / 2
return x,y
def latlon2xy(lat, lon, z):
n = numTiles(z)
x, y = latlon2relativeXY(lat, lon)
return n * x, n * y
def tileXY(lat, lon, z):
x, y = latlon2xy(lat, lon, z)
return int(x), int(y)
def xy2latlon(x, y, z):
n = numTiles(z)
relY = y / n
lat = mercatorToLat(pi * (1 - 2 * relY))
lon = -180 + 360 * x / n
return lat, lon
def mercatorToLat(mercatorY):
return degrees(atan(sinh(mercatorY)))
def get_tile_grid(x, y, width, height, ntiles):
def get_indices(x, width, ntiles):
def _(x, width, p, ntiles, xs=None, ns=None):
'''p == 0: backward
p == 1: forward
Must be called with p=0 then p=1
'''
x2 = (x % 1) * 256
if p == 0:
xs = [width / 2 - x2]
ns = [int(x)]
else:
x2 = 256 - x2
done = False
while not done:
if x2 >= width / 2:
# out of canvas, don't show next tile
done = True
else:
# show (part of) next tile
x2 += 256
if p == 0:
n1 = ns[-1] - 1
x1 = xs[-1] - 256
if n1 < 0:
done = True
else:
ns.append(n1)
xs.append(x1)
else:
n1 = ns[-1] + 1
x1 = xs[-1] + 256
if n1 >= ntiles:
done = True
else:
ns.append(n1)
xs.append(x1)
if p == 0:
xs = xs[::-1]
ns = ns[::-1]
return xs, ns
xs, ns = _(x, width, 0, ntiles)
xs, ns = _(x, width, 1, ntiles, xs, ns)
return xs, ns
xs, xn = get_indices(x, width, ntiles)
ys, yn = get_indices(y, height, ntiles)
def get_grid(xs, ys):
xys = []
for j in ys:
xys.append([])
for i in xs:
xys[-1].append((i, j))
return xys
xys = get_grid(xs, ys)
xyn = get_grid(xn, yn)
return {'pix': xys, 'tile': xyn}
class Map(MultiCanvas):
def __init__(self, width=256, height=256, zoom=0, lat=0, lon=0):
super(Map, self).__init__(2, width=width, height=height)
self.z = zoom
self.tile_nb = numTiles(zoom)
self.height = height
self.width = width
self.dragging = False
self.lat = lat
self.lon = lon
self.x, self.y = latlon2xy(lat, lon, zoom)
self.show(lat, lon)
self[1].on_mouse_down(self.mouse_down_handler)
self[1].on_mouse_move(self.mouse_move_handler)
self[1].on_mouse_up(self.mouse_up_handler)
self[1].on_mouse_out(self.mouse_out_handler)
def show(self, lat=None, lon=None):
if lat is None:
lat = self.lat
if lon is None:
lon = self.lon
x, y = latlon2xy(lat, lon, self.z)
grid = get_tile_grid(x, y, self.width, self.height, self.tile_nb)
with hold_canvas(self):
self[0].fill_style = '#aad3df'
self[0].fill_rect(0, 0, self.width, self.height)
for i, row in enumerate(grid['pix']):
for j, col in enumerate(row):
dx, dy = col
x, y = grid['tile'][i][j]
url = f'https://tile.openstreetmap.org/{self.z}/{x}/{y}.png'
tile = Image.from_url(url)
self[0].draw_image(tile, dx, dy)
def mouse_down_handler(self, pixel_x, pixel_y):
self.dragging = True
self.x_mouse = pixel_x
self.y_mouse = pixel_y
@debounce(0.01)
def mouse_move_handler(self, pixel_x, pixel_y):
if self.dragging:
x, y, lat, lon = self.delta(pixel_x, pixel_y)
self.show(lat, lon)
def delta(self, pixel_x, pixel_y):
dx = (pixel_x - self.x_mouse) / 256
dy = (pixel_y - self.y_mouse) / 256
x = self.x - dx
y = self.y - dy
lat, lon = xy2latlon(x, y, self.z)
return x, y, lat, lon
def mouse_up_handler(self, pixel_x, pixel_y):
self.dragging = False
self.x, self.y, self.lat, self.lon = self.delta(pixel_x, pixel_y)
self.show()
def mouse_out_handler(self, pixel_x, pixel_y):
self.dragging = False
m = Map(788, 788, 7, 49, 2)
m