148 lines
4.5 KiB
Python
148 lines
4.5 KiB
Python
|
|
"""rio-tiler dynamic tile server for I7D16.tif"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from fastapi import FastAPI, HTTPException
|
|||
|
|
from fastapi.responses import HTMLResponse
|
|||
|
|
from starlette.requests import Request
|
|||
|
|
from starlette.responses import Response
|
|||
|
|
|
|||
|
|
from rio_tiler.errors import TileOutsideBounds
|
|||
|
|
from rio_tiler.io import Reader
|
|||
|
|
from rio_tiler.profiles import img_profiles
|
|||
|
|
|
|||
|
|
DATA_PATH = str(Path(__file__).parent / "data" / "I7D16.tif")
|
|||
|
|
|
|||
|
|
# Per-band rescale range derived from p2/p98 statistics of the source image
|
|||
|
|
# Band order: Red, Green, Blue (band 4 NIR is ignored)
|
|||
|
|
RESCALE = [
|
|||
|
|
(6130, 40453), # Red
|
|||
|
|
(10641, 41211), # Green
|
|||
|
|
(12482, 36323), # Blue
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
app = FastAPI(
|
|||
|
|
title="rio-tiler",
|
|||
|
|
description="Dynamic tile server for I7D16.tif (NJ 2020 aerial imagery)",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get(
|
|||
|
|
"/tiles/{z}/{x}/{y}.png",
|
|||
|
|
responses={
|
|||
|
|
200: {"content": {"image/png": {}}, "description": "Return a PNG tile."},
|
|||
|
|
404: {"description": "Tile outside image bounds."},
|
|||
|
|
},
|
|||
|
|
response_class=Response,
|
|||
|
|
description="Read COG tile and return a PNG image (RGB bands, UInt16 rescaled)",
|
|||
|
|
)
|
|||
|
|
def tile(z: int, x: int, y: int):
|
|||
|
|
"""Return a map tile for the given z/x/y coordinates."""
|
|||
|
|
try:
|
|||
|
|
with Reader(DATA_PATH) as cog:
|
|||
|
|
img = cog.tile(x, y, z, indexes=(1, 2, 3))
|
|||
|
|
except TileOutsideBounds:
|
|||
|
|
raise HTTPException(status_code=404, detail="Tile outside image bounds")
|
|||
|
|
|
|||
|
|
content = img.render(
|
|||
|
|
img_format="PNG",
|
|||
|
|
rescale=RESCALE,
|
|||
|
|
**img_profiles.get("png", {}),
|
|||
|
|
)
|
|||
|
|
return Response(content, media_type="image/png")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get("/tilejson.json", responses={200: {"description": "Return a TileJSON document"}})
|
|||
|
|
def tilejson(request: Request):
|
|||
|
|
"""Return a TileJSON 2.2.0 document describing the available tiles."""
|
|||
|
|
with Reader(DATA_PATH) as cog:
|
|||
|
|
bounds = cog.get_geographic_bounds(cog.tms.rasterio_geographic_crs)
|
|||
|
|
minzoom = cog.minzoom
|
|||
|
|
maxzoom = cog.maxzoom
|
|||
|
|
|
|||
|
|
base_url = str(request.base_url).rstrip("/")
|
|||
|
|
tile_url = f"{base_url}/tiles/{{z}}/{{x}}/{{y}}.png"
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"tilejson": "2.2.0",
|
|||
|
|
"name": os.path.basename(DATA_PATH),
|
|||
|
|
"description": "NJ Open Imagery 2020 - NAD83(2011) / New Jersey (ftUS)",
|
|||
|
|
"tiles": [tile_url],
|
|||
|
|
"bounds": list(bounds),
|
|||
|
|
"minzoom": minzoom,
|
|||
|
|
"maxzoom": maxzoom,
|
|||
|
|
"center": [
|
|||
|
|
(bounds[0] + bounds[2]) / 2,
|
|||
|
|
(bounds[1] + bounds[3]) / 2,
|
|||
|
|
minzoom + 2,
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.get("/", response_class=HTMLResponse)
|
|||
|
|
def index(request: Request):
|
|||
|
|
"""Simple Leaflet.js viewer for the imagery."""
|
|||
|
|
with Reader(DATA_PATH) as cog:
|
|||
|
|
bounds = cog.get_geographic_bounds(cog.tms.rasterio_geographic_crs)
|
|||
|
|
minzoom = cog.minzoom
|
|||
|
|
maxzoom = cog.maxzoom
|
|||
|
|
|
|||
|
|
base_url = str(request.base_url).rstrip("/")
|
|||
|
|
tile_url = f"{base_url}/tiles/{{z}}/{{x}}/{{y}}.png"
|
|||
|
|
|
|||
|
|
center_lat = (bounds[1] + bounds[3]) / 2
|
|||
|
|
center_lng = (bounds[0] + bounds[2]) / 2
|
|||
|
|
init_zoom = minzoom + 2
|
|||
|
|
|
|||
|
|
return f"""<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<title>I7D16 Tile Viewer</title>
|
|||
|
|
<meta charset="utf-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|||
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|||
|
|
<style>
|
|||
|
|
html, body, #map {{ height: 100%; margin: 0; padding: 0; }}
|
|||
|
|
#info {{
|
|||
|
|
position: absolute; top: 10px; right: 10px; z-index: 1000;
|
|||
|
|
background: white; padding: 10px 14px; border-radius: 4px;
|
|||
|
|
font-family: sans-serif; font-size: 13px;
|
|||
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="map"></div>
|
|||
|
|
<div id="info">
|
|||
|
|
<b>I7D16.tif</b><br>
|
|||
|
|
NJ Open Imagery 2020<br>
|
|||
|
|
Zoom: {minzoom}–{maxzoom}<br>
|
|||
|
|
CRS: NAD83(2011) / NJ ftUS
|
|||
|
|
</div>
|
|||
|
|
<script>
|
|||
|
|
var map = L.map('map').setView([{center_lat}, {center_lng}], {init_zoom});
|
|||
|
|
|
|||
|
|
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
|||
|
|
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>',
|
|||
|
|
opacity: 0.4,
|
|||
|
|
maxZoom: 19
|
|||
|
|
}}).addTo(map);
|
|||
|
|
|
|||
|
|
L.tileLayer('{tile_url}', {{
|
|||
|
|
minZoom: {minzoom},
|
|||
|
|
maxZoom: {maxzoom},
|
|||
|
|
opacity: 1.0,
|
|||
|
|
attribution: 'NJ Open Imagery 2020'
|
|||
|
|
}}).addTo(map);
|
|||
|
|
|
|||
|
|
map.fitBounds([
|
|||
|
|
[{bounds[1]}, {bounds[0]}],
|
|||
|
|
[{bounds[3]}, {bounds[2]}]
|
|||
|
|
]);
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>"""
|