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

4.1 KiB
Raw Blame History

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

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 and put nginx in front with 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):

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.