Files
homepage/tile-proxy/README.md
T
Alexander 2347a02fcb 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.
2026-05-22 16:26:22 +02:00

97 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, DouglasPeuckersimplifies 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.