Created
November 30, 2022 02:57
-
-
Save darrenwiens/222c0d0404540afeeea922a786e66420 to your computer and use it in GitHub Desktop.
Mapbox single tile map client and API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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