Skip to content

Instantly share code, notes, and snippets.

@darrenwiens
Created November 30, 2022 02:57
Show Gist options
  • Save darrenwiens/222c0d0404540afeeea922a786e66420 to your computer and use it in GitHub Desktop.
Save darrenwiens/222c0d0404540afeeea922a786e66420 to your computer and use it in GitHub Desktop.
Mapbox single tile map client and API
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single tile</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script src="scripts/tilebelt.js"></script> <!-- was too lazy to figure out how to install tilebelt: https://github.com/mapbox/tilebelt/blob/master/index.js -->
<style>
body {
margin: 0;
padding: 0;
background-color: black;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_API_KEY';
let apiRoot = "http://127.0.0.1:8000"
let tileFC;
let map;
let gui = new dat.GUI();
let guiObject = function () {
this.lat = 45;
this.lng = -122;
this.zoom = 13;
this.add = updateMapView;
this.sides = 30;
this.color = "#78683b";
this.fog = false;
};
let guiInstance = new guiObject();
let params = gui.addFolder("Params");
params.add(guiInstance, "lat", -90, 90, 0.1).name('Latitude');
params.add(guiInstance, "lng", -180, 180, 0.1).name('Longitude');
params.add(guiInstance, "zoom", 0, 18, 1).name('Zoom');
params.add(guiInstance, "sides", 0, 1000, 1).name('Side Height').onChange(function (value) {
updateSides(value);
});
params.addColor(guiInstance, "color").name('Side Color').onChange(function (value) {
updateSideColor(value);
});
params.add(guiInstance, "fog").name('Fog').onChange(function (value) {
updateFog(value);
});;
params.add(guiInstance, 'add').name('Update Map View');
function updateMapView() {
let mapCenter = new mapboxgl.LngLat(guiInstance.lng, guiInstance.lat);
let zoom = guiInstance.zoom;
let pitch = 60;
let bearing = 0;
drawMap(mapCenter, zoom, pitch, bearing);
}
function updateSides(bufferVal) {
let curTile = pointToTile(guiInstance.lng, guiInstance.lat, guiInstance.zoom)
let tileGeom = tileToGeoJSON(curTile)
let tileBnds = turf.buffer(tileGeom, bufferVal / 2, { units: 'meters' });
let tileLine = turf.polygonToLine(tileBnds);
let tileLineBuff = turf.buffer(tileLine, bufferVal, { units: 'meters' });
map.getSource("sides").setData(turf.featureCollection([tileLineBuff]));
}
function updateSideColor(color) {
map.setPaintProperty("sides", 'fill-color', color);
}
function updateFog(value) {
let fog = value ? {
'horizon-blend': 0.3,
'color': '#f8f0e3',
'high-color': '#add8e6',
'space-color': '#d8f2ff',
'star-intensity': 0.0
} : null
map.setFog(fog);
}
function drawMap(mapCenter, zoom, pitch, bearing) {
document.getElementById("map").outerHTML = "";
mapDivEl = document.createElement("div")
mapDivEl.id = "map"
document.body.prepend(mapDivEl);
map = new mapboxgl.Map({
container: 'map',
zoom: zoom,
center: mapCenter,
pitch: pitch,
bearing: bearing,
style: 'mapbox://styles/mapbox/satellite-v9',
transformRequest: (url, resourceType) => {
let tile = pointToTile(guiInstance.lng, guiInstance.lat, guiInstance.zoom)
if (resourceType === 'Tile' && url.indexOf('mapbox.satellite') > -1) {
if (url.indexOf(`${tile[2]}/${tile[0]}/${tile[1]}`) == -1) {
return {
url: null
};
} else {
let centerTile = [tile[0], tile[1], tile[2]]
let tileGeom = tileToGeoJSON(tile)
let bufferVal = 30
let tileBnds = turf.buffer(tileGeom, bufferVal / 2, { units: 'meters' });
let tileLine = turf.polygonToLine(tileBnds);
let tileLineBuff = turf.buffer(tileLine, bufferVal, { units: 'meters' });
tileFC = turf.featureCollection([tileLineBuff])
if (map.getSource("sides")) {
map.getSource("sides").setData(tileFC);
}
return {
url: url
};
}
} else {
if (resourceType === 'Tile' && url.indexOf(apiRoot) > -1) {
let apiUrl = url + `?map_tile=${tile[0]},${tile[1]},${tile[2]}`
return {
url: apiUrl
};
}
}
}
});
map.on('load', () => {
map.addSource("augmented-dem", {
type: "raster-dem",
tiles: [
`${apiRoot}/{z}/{x}/{y}`
],
tileSize: 256,
encoding: "terrarium",
});
map.setTerrain({ source: "augmented-dem", exaggeration: 1 });
map.addSource('sides', {
'type': 'geojson',
'data': tileFC
});
map.addLayer({
'id': 'sides',
'type': 'fill',
'source': 'sides',
'layout': {},
'paint': {
'fill-color': guiInstance.color,
'fill-opacity': 1
}
});
});
}
updateMapView()
</script>
</body>
</html>
import io
import mercantile
import pyproj
import numpy as np
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse
from PIL import Image
from shapely.geometry import Polygon
import rasterio
from rasterio.mask import mask
app = FastAPI()
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/{z}/{x}/{y}")
async def read_item(x, y, z, map_tile):
print('=========== New tile ============', x, y, z, map_tile)
map_tile = mercantile.Tile(*[int(i) for i in map_tile.split(",")])
map_tile_shape = mercantile.feature(map_tile)
map_tile_coords = [list(l) for l in map_tile_shape["geometry"]["coordinates"][0]]
map_tile_poly = Polygon(map_tile_coords)
print('map tile', map_tile, map_tile_poly)
terrain_tile = mercantile.Tile(x=int(x), y=int(y), z=int(z))
terrain_tile_shape = mercantile.feature(terrain_tile)
terrain_tile_coords = [list(l) for l in terrain_tile_shape["geometry"]["coordinates"][0]]
terrain_tile_poly = Polygon(terrain_tile_coords)
print("terrain_tile", terrain_tile, terrain_tile_poly)
if map_tile_poly.intersects(terrain_tile_poly):
geotiff_url = f"https://elevation-tiles-prod.s3.amazonaws.com/v2/geotiff/{terrain_tile.z}/{terrain_tile.x}/{terrain_tile.y}.tif"
with rasterio.open(geotiff_url) as geotiff_src:
try:
proj = pyproj.Transformer.from_crs(
4326, geotiff_src.crs.to_epsg(), always_xy=True
)
proj_poly = Polygon([proj.transform(*i) for i in map_tile_poly.exterior.coords])
msk = mask(
geotiff_src,
[proj_poly],
all_touched=False,
invert=False,
nodata=None,
filled=True,
# crop=True,
pad=False,
pad_width=0.5,
indexes=None,
)
dem = msk[0].squeeze()
dem[dem == geotiff_src.nodata] = 0
r = np.zeros(dem.shape)
g = np.zeros(dem.shape)
b = np.zeros(dem.shape)
v = dem + 32768
r += np.floor(v / 256.0)
g += np.floor(v % 256.0)
b += np.floor((v - np.floor(v)) * 256.0)
stack = np.dstack(
[
r.astype(np.uint8),
g.astype(np.uint8),
b.astype(np.uint8),
]
)
except ValueError as e:
print("value error", e)
stack = np.zeros([256, 256, 4], dtype=np.uint8)
else:
print("DISJOINT")
stack = np.zeros([256, 256, 4], dtype=np.uint8)
im = Image.fromarray(stack)
img_bytes = io.BytesIO()
im.save(img_bytes, "PNG")
img_bytes.seek(0)
headers = {
"Content-Type": "image/png",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS,GET",
"Access-Control-Allow-Headers": "Content-Type",
}
return StreamingResponse(img_bytes, headers=headers)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment