feat(hikes): worldwide maps via a region-switching tile proxy
Add tile-proxy/: a small Rust (axum) service behind nginx that serves one
canonical XYZ scheme (/{karte,luftbild,dufour}/{z}/{x}/{y}) and, per tile,
picks the provider by geometry — swisstopo when the tile overlaps a
swisstopo-covered region (Switzerland or Liechtenstein, each simplified +
2 km buffer; tile-bbox ∩ polygon at every zoom), otherwise OpenTopoMap
(schematic) / Esri World Imagery (satellite), with an auto-fallback for
border 404s. Includes the region generator (gen-regions.mjs), a Makefile,
nginx caching-proxy + systemd examples, and a README. Listen address is
env-driven (TILE_PROXY_ADDR).
App side:
- New mapTiles.ts is the single source for the proxy URLs + combined
attribution; HikeMap / HikesOverviewMap / EditMap fetch through
maps.bocken.org instead of swisstopo directly, on-map attribution
controls removed, preconnect + footer credits updated (swisstopo /
OpenStreetMap+OpenTopoMap / Esri).
- Region-aware schematic max zoom (isSwissRegion helper): detail map caps
at z17 abroad and hides the CH/LI-only Dufour layer; overview caps at
z18 when a shown hike is abroad.
- Route-builder: add the satellite layer via the same bottom-right layer
popover as the other maps.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# tile-proxy
|
||||
|
||||
A tiny region-switching map-tile service for the hikes maps. It exposes one
|
||||
canonical XYZ scheme and, **per tile**, serves **swisstopo inside Switzerland
|
||||
& Liechtenstein** and a **global provider elsewhere** — so a hike anywhere in
|
||||
the world gets a good schematic + satellite basemap, while CH/LI hikes keep
|
||||
swisstopo quality.
|
||||
|
||||
```
|
||||
browser / build script
|
||||
│ https://maps.bocken.org/{layer}/{z}/{x}/{y}
|
||||
▼
|
||||
nginx ── TLS termination + on-disk tile cache (proxy_cache)
|
||||
▼ http://$TILE_PROXY_ADDR (default 127.0.0.1:8765)
|
||||
tile-proxy (this crate) ── z/x/y → point-in-polygon → pick + fetch upstream
|
||||
▼
|
||||
swisstopo │ OpenTopoMap │ Esri World Imagery
|
||||
```
|
||||
|
||||
The listen address is set entirely by the **`TILE_PROXY_ADDR`** env var
|
||||
(`host:port`); pick whatever port you like. nginx, the systemd unit and this
|
||||
binary all reference that one value.
|
||||
|
||||
Caching/TLS/rate-limiting live in **nginx** (Arch's stock nginx has no njs, and
|
||||
we want a real geometry test anyway). This binary only does routing + fetch and
|
||||
is stateless.
|
||||
|
||||
## Layers & providers
|
||||
|
||||
| `{layer}` | swisstopo region (CH + LI) | elsewhere |
|
||||
|------------|----------------------------------|-----------------------|
|
||||
| `karte` | `ch.swisstopo.pixelkarte-farbe` | OpenTopoMap |
|
||||
| `luftbild` | `ch.swisstopo.swissimage` | Esri World Imagery |
|
||||
| `dufour` | `ch.swisstopo.hiks-dufour` | — (CH/LI-only) |
|
||||
|
||||
The swisstopo region is **Switzerland + Liechtenstein** (swisstopo has
|
||||
high-quality data for both). A tile is served by swisstopo when it **overlaps**
|
||||
that region at all — the tile's lat/lng rectangle is intersected against the
|
||||
polygons (`src/regions.in`, simplified + a **2 km outward buffer**), so *any*
|
||||
tile touching CH/LI gets swisstopo, at every zoom. (A swisstopo tile partly
|
||||
outside its data just renders white at the edges, which beats a foreign
|
||||
provider drawing over covered ground.) For `karte`/`luftbild`, a swisstopo tile
|
||||
that 404s near the edge falls back to the global provider automatically.
|
||||
|
||||
## Build & run
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
TILE_PROXY_ADDR=127.0.0.1:8765 ./target/release/tile-proxy
|
||||
# smoke test
|
||||
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
|
||||
http://127.0.0.1:8765/karte/9/266/180 # Bern → swisstopo (jpeg)
|
||||
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
|
||||
http://127.0.0.1:8765/karte/9/255/171 # London → OpenTopoMap (png)
|
||||
```
|
||||
|
||||
`TILE_PROXY_ADDR` defaults to `127.0.0.1:8765` but should be set explicitly.
|
||||
`GET /healthz` returns `ok`.
|
||||
|
||||
Run it as a service with [`deploy/tile-proxy.service`](deploy/tile-proxy.service)
|
||||
and put nginx in front with [`deploy/nginx.conf.example`](deploy/nginx.conf.example).
|
||||
|
||||
## Regenerating the region polygons
|
||||
|
||||
`src/regions.in` is generated — don't hand-edit it. To refresh (or change the
|
||||
buffer / simplification, or add a region):
|
||||
|
||||
```sh
|
||||
make regions # CH + LI, default 2 km buffer
|
||||
BUFFER_KM=2 SIMPLIFY_DEG=0.004 node scripts/gen-regions.mjs
|
||||
node scripts/gen-regions.mjs ./ch.geojson ./li.geojson # local files
|
||||
```
|
||||
|
||||
It takes each source's largest exterior ring, Douglas–Peucker–simplifies it,
|
||||
then pushes every vertex outward by `BUFFER_KM`. Rebuild the binary afterwards
|
||||
(the ring is baked in via `include!`). If you move the boundary, purge the nginx
|
||||
cache so old tiles aren't served by the previous provider.
|
||||
|
||||
## Attribution (required)
|
||||
|
||||
All three providers require credit — keep these on the page (the hikes pages
|
||||
show them in the footer):
|
||||
|
||||
- **© swisstopo** — Swiss tiles.
|
||||
- **© OpenStreetMap contributors, SRTM | © OpenTopoMap (CC-BY-SA)** — world schematic.
|
||||
- **© Esri, Maxar, Earthstar Geographics** — world satellite.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Max zoom** differs by provider — clamp the client: OpenTopoMap 17,
|
||||
Esri imagery ~19, swisstopo 19/20.
|
||||
- **OpenTopoMap fair-use:** it's a small volunteer project; the long nginx
|
||||
cache + a descriptive `User-Agent` is the mitigation. If traffic grows,
|
||||
self-host a renderer.
|
||||
- **Dufour** has no world equivalent — the client hides that layer when the
|
||||
view leaves Switzerland.
|
||||
Reference in New Issue
Block a user