OpenTopoMap's hypsometric tint reads "red mountains / green flats" and looks nothing like the Swisstopo Pixelkarte the proxy hands out in-region — produced a jarring visual seam right at the CH/LI border. Thunderforest Outdoors has a muted topo palette + subtle hillshade that matches the swisstopo tile aesthetic much more closely, so use it as the abroad `karte` upstream when an API key is available. - `tile-proxy/build.rs`: reads `tile-proxy/.env` (gitignored) at build time and forwards each `KEY=VAL` line to rustc as `--env`, so the key is baked into the binary via `option_env!` and never touched at runtime. A shell env var of the same name wins over the .env entry (dotenv precedence). `cargo:rerun-if-changed=.env` + `cargo:rerun-if-env-changed` force a recompile whenever the value changes — no stale key cached in the binary. - `main.rs`: `THUNDERFOREST_API_KEY` read via `option_env!`; foreign `karte` is Thunderforest Outdoors when set, OpenTopoMap fallback when absent. Behaviour unchanged for keyless builds. - `mapTiles.ts`: page-footer attribution credits Thunderforest + OSM alongside the existing swisstopo / OpenTopoMap / Esri lines so the attribution stays correct regardless of which build is deployed. - `.gitignore`: tile-proxy build artefacts (binary, `target/`, `.env`) moved to the root gitignore with fully-qualified paths so the source tree isn't hidden by a nested gitignore quirk; the per-dir `tile-proxy/.gitignore` is removed. - README + systemd service: documentation refreshed for the new build-time key flow.
5.1 KiB
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 |
Thunderforest Outdoors (or OpenTopoMap) |
luftbild |
ch.swisstopo.swissimage |
Esri World Imagery |
dufour |
ch.swisstopo.hiks-dufour |
— (CH/LI-only) |
The karte upstream abroad is Thunderforest Outdoors when a
THUNDERFOREST_API_KEY is available at build time — it's baked into the
binary via option_env!. The key is read from tile-proxy/.env (gitignored)
by build.rs, or from a shell env var of the same name; both are watched so
the binary recompiles whenever the value changes. Without a key, the build
falls back to OpenTopoMap (no key needed, but its hypsometric tint reads
as "red mountains / green flats", which is why Thunderforest is preferred
when available).
# tile-proxy/.env (gitignored)
THUNDERFOREST_API_KEY=your-key-here
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
# Optional: drop a Thunderforest key in tile-proxy/.env for nicer abroad
# `karte` tiles; the build falls back to OpenTopoMap when the file is absent.
echo 'THUNDERFOREST_API_KEY=your-key-here' > .env
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 → Thunderforest / 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, 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 providers require credit — keep these on the page (the hikes pages show them in the footer):
- © swisstopo — Swiss tiles.
- Maps © Thunderforest, Data © OpenStreetMap contributors — world schematic (when the key is baked in).
- © OpenStreetMap contributors, SRTM | © OpenTopoMap (CC-BY-SA) — world schematic (fallback build).
- © 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-Agentis the mitigation. If traffic grows, self-host a renderer. - Dufour has no world equivalent — the client hides that layer when the view leaves Switzerland.