94c8212078
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.
115 lines
5.1 KiB
Markdown
115 lines
5.1 KiB
Markdown
# 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).
|
||
|
||
```sh
|
||
# 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
|
||
|
||
```sh
|
||
# 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`](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 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-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.
|