simonw / datasette-tiles

Mapping tile server for Datasette, serving tiles from MBTiles packages
https://datasette.io/plugins/datasette-tiles
7 stars 5 forks source link

Endpoint providing a static maps API that stitches tiles together for you #17

Open simonw opened 3 years ago

simonw commented 3 years ago

I love the Google Maps Static API - https://developers.google.com/maps/documentation/maps-static/overview - both for covering non-JavaScript cases and because it's a much more efficient way to drop a simple map on a page without loading a JavaScript library.

It strikes me that a very simple version of that - where you pass a size, bounding box and maybe even a marker or two - could be supported by this plugin. Loading a few tiles from SQLite, stitching them together into a new image using Pillow and serving the result feels like it might not be a big push.

And then people could serve their own version of the static maps API using their own tiles!

Originally posted by @simonw in https://github.com/simonw/datasette-tiles/issues/14#issuecomment-773440881

simonw commented 3 years ago

I'd rather not add Pillow as a required dependency, but this feature could treat it as optional - you need to install Pillow for this feature to work but without Pillow other functionality remains.

simonw commented 3 years ago

Initial URL design:

/-/tiles-static?center=latitude,longitude
  &zoom=zoom
  &size=WIDTHxHEIGHT

Google let you pass a string to center= which they'll geocode for you - maybe provide a mechanism for optionally configuring a geocoder in the future. For the first version supporting just latitude,longitude makes sense.

simonw commented 3 years ago

Prior art: https://wiki.openstreetmap.org/wiki/Tile_stitching and https://wiki.openstreetmap.org/wiki/Static_map_images

https://github.com/flopp/py-staticmaps is an impressive Python implementation of this idea. It uses pycairo which may be a tricky dependency (or maybe it's as easy to install as Pillow, I've not tried yet).

simonw commented 3 years ago

Looking at https://github.com/jperelli/osm-static-maps reminded me that this will need to be able to add attribution to the images too, e.g. http://osm-static-maps.herokuapp.com/?height=200

image

simonw commented 3 years ago

Stitching tiles together in PIL is pretty straight-forward:

from PIL import Image
import httpx
import io

top = "https://datasette-tiles-demo.datasette.io/-/tiles/basemap/1/0/1.png"
bottom = "https://datasette-tiles-demo.datasette.io/-/tiles/basemap/1/0/0.png"

top_png = httpx.get(top).content
bottom_png = httpx.get(bottom).content

# Create a new blank image 256x512
new = image = Image.new('RGB', (256, 512))

new.paste(top_im, (0, 0, 256, 256))
new.paste(bottom_im, (0, 256, 256, 512))

new.save("/tmp/stitched.png")

Image generated by this:

stitched

simonw commented 3 years ago

https://pygeotile.readthedocs.io/en/latest/ can be used to convert lat/lon to tile co-ordinates and back again.

For a point within San Francisco:

point = Point.from_latitude_longitude(37.777, -122.426)
tile = Tile.for_latitude_longitude(37.777, -122.426, 6)
# Tile is now Tile(tms_x=10, tms_y=39, zoom=6)
tile_url = "https://datasette-tiles-demo.datasette.io/-/tiles/basemap/{z}/{x}/{y}.png".format(
  z=tile.zoom, x=tile.tms_x, y=tile.tms_y
)

This gives me tile_url of https://datasette-tiles-demo.datasette.io/-/tiles/basemap/6/10/39.png

image

The point.pixels(zoom) method then provides:

point.pixels(6)
# Outputs (2620, 6333)
tile.bounds
# Outputs:
# (Point(latitude=36.5978891330702, longitude=-123.75),
#  Point(latitude=40.979898069620155, longitude=-118.125))
tile.bounds[0].pixels(6), tile.bounds[1].pixels(6)
# Outputs ((2560, 6400), (2816, 6144))
simonw commented 3 years ago

Because I find diagrams useful, here's how those "pixel" coordinates work:

Banners_and_Alerts_and_Map_tile_coordinate_systems_–_Figma