diff --git a/.gitignore b/.gitignore index 2a43c834..be63b047 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,13 @@ static/shopping/cumulus.svg static/hikes/ hikes-assets/ src/lib/data/hikes.generated.ts +# Tile-proxy build artefacts + secrets (the source tree itself is tracked). +# The binary is dropped next to Cargo.toml by `make build`; its name happens +# to collide with the directory it lives in, so the path is fully qualified +# here to avoid the nested-gitignore quirk that previously hid the source. +/tile-proxy/tile-proxy +/tile-proxy/target/ +/tile-proxy/.env # Private image build outputs (regenerated by scripts/build-private-images.ts). # Sources are private + large, so they're ignored too — only the README is kept. private-assets/ diff --git a/package.json b/package.json index bed5121c..4f4eb276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.93.0", + "version": "1.94.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/data/mapTiles.ts b/src/lib/data/mapTiles.ts index 8d4569ca..2c237192 100644 --- a/src/lib/data/mapTiles.ts +++ b/src/lib/data/mapTiles.ts @@ -21,10 +21,14 @@ export const TILE_URL = { } as const; /** Combined attribution — the proxy may serve any provider depending on the - * region in view, so all three are credited. Shown in the page footer (the - * on-map control is disabled). */ + * region in view, so all are credited. Shown in the page footer (the on-map + * control is disabled). Thunderforest is the primary `karte` upstream abroad + * when the proxy was built with a `THUNDERFOREST_API_KEY`; OpenTopoMap is + * the no-key fallback. Both are credited so the attribution stays correct + * regardless of which build is deployed. */ export const TILE_ATTRIBUTION = '© swisstopo · ' + - '© OpenStreetMap, ' + + 'Maps © Thunderforest, ' + + 'Data © OpenStreetMap · ' + 'OpenTopoMap · ' + '© Esri'; diff --git a/tile-proxy/.gitignore b/tile-proxy/.gitignore deleted file mode 100644 index 0c50b62f..00000000 --- a/tile-proxy/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -/tile-proxy diff --git a/tile-proxy/README.md b/tile-proxy/README.md index 80fa32fa..4a9a9fb5 100644 --- a/tile-proxy/README.md +++ b/tile-proxy/README.md @@ -27,11 +27,25 @@ 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) | +| `{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** @@ -45,13 +59,16 @@ 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 → OpenTopoMap (png) + 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. @@ -78,11 +95,12 @@ 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): +All 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. +- **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 diff --git a/tile-proxy/build.rs b/tile-proxy/build.rs new file mode 100644 index 00000000..5f5347dd --- /dev/null +++ b/tile-proxy/build.rs @@ -0,0 +1,45 @@ +// Reads `tile-proxy/.env` (gitignored, KEY=VALUE per line, `#` comments) and +// forwards each entry to rustc as `--env KEY=VALUE`, so `option_env!("KEY")` +// in main.rs picks the value up at compile time. This keeps secrets like +// THUNDERFOREST_API_KEY out of the shell and out of git — the key is read +// once at build time and baked into the binary; the running process never +// touches the file. +// +// `cargo:rerun-if-changed=.env` forces a recompile whenever the file is +// edited (set, unset, rotated), so a stale key never lingers in the cached +// binary. We also watch the env var of the same name so explicit +// `THUNDERFOREST_API_KEY=... cargo build` still works (env var wins over +// .env, mirroring how dotenv-style tools behave). +use std::fs; + +fn main() { + println!("cargo:rerun-if-changed=.env"); + println!("cargo:rerun-if-env-changed=THUNDERFOREST_API_KEY"); + + let Ok(contents) = fs::read_to_string(".env") else { + return; // .env is optional — the binary falls back to OpenTopoMap. + }; + + for raw in contents.lines() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + // Strip optional surrounding quotes around the value, then trim. + let value = value.trim().trim_matches('"').trim_matches('\''); + if key.is_empty() { + continue; + } + // Explicit shell env var beats the .env entry — same precedence as + // standard dotenv tooling. std::env::var here reflects what was set + // on the cargo invocation. + if std::env::var_os(key).is_some() { + continue; + } + println!("cargo:rustc-env={key}={value}"); + } +} diff --git a/tile-proxy/deploy/tile-proxy.service b/tile-proxy/deploy/tile-proxy.service index 26fd6ef0..8b8d16e3 100644 --- a/tile-proxy/deploy/tile-proxy.service +++ b/tile-proxy/deploy/tile-proxy.service @@ -1,6 +1,9 @@ # systemd unit for the tile proxy. # install: cp deploy/tile-proxy.service /etc/systemd/system/ -# (build first: cargo build --release; adjust paths/user below) +# (build first: drop the Thunderforest key into tile-proxy/.env +# and run `cargo build --release`; build.rs reads .env and bakes +# the key into the binary at compile time, nothing is read at +# runtime; adjust paths/user below) # enable: systemctl daemon-reload && systemctl enable --now tile-proxy [Unit] diff --git a/tile-proxy/src/main.rs b/tile-proxy/src/main.rs index f724d0c4..a6cfe615 100644 --- a/tile-proxy/src/main.rs +++ b/tile-proxy/src/main.rs @@ -5,11 +5,18 @@ //! the tile overlaps a swisstopo-covered region (Switzerland or Liechtenstein, //! each buffered ~2 km) and a global provider otherwise: //! -//! | 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 historical) | +//! | layer | swisstopo region (CH + LI) | elsewhere | +//! |----------|------------------------------|--------------------------------------| +//! | karte | ch.swisstopo.pixelkarte-farbe| Thunderforest Outdoors (or OpenTopo) | +//! | luftbild | ch.swisstopo.swissimage | Esri World Imagery | +//! | dufour | ch.swisstopo.hiks-dufour | — (CH/LI-only historical) | +//! +//! The `karte` upstream abroad is Thunderforest Outdoors when the +//! `THUNDERFOREST_API_KEY` env var is set at *build* time (it's baked into +//! the binary via `option_env!`), otherwise OpenTopoMap (whose hypsometric +//! tint reads "red mountains / green flats"). `build.rs` reads the key from +//! `tile-proxy/.env` (gitignored) or a shell env var, and requests a +//! recompile when either changes. //! //! Caching, TLS and rate-limiting live in nginx in front of this service //! (see README.md) — this binary only does the routing + upstream fetch. @@ -133,13 +140,21 @@ fn upstreams(layer: &str, z: u32, x: u32, y: u32) -> Option<(String, Option format!("https://tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey={key}"), + None => opentopo(), + }; // NB: Esri uses {z}/{y}/{x} (row before column). let esri = || { format!("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}") }; match layer { - "karte" if swiss => Some((swisstopo("ch.swisstopo.pixelkarte-farbe", "jpeg", z, x, y), Some(opentopo()))), - "karte" => Some((opentopo(), None)), + "karte" if swiss => Some((swisstopo("ch.swisstopo.pixelkarte-farbe", "jpeg", z, x, y), Some(outdoors_abroad()))), + "karte" => Some((outdoors_abroad(), None)), "luftbild" if swiss => Some((swisstopo("ch.swisstopo.swissimage", "jpeg", z, x, y), Some(esri()))), "luftbild" => Some((esri(), None)), // Historical Dufour map only exists for Switzerland. @@ -148,6 +163,12 @@ fn upstreams(layer: &str, z: u32, x: u32, y: u32) -> Option<(String, Option = option_env!("THUNDERFOREST_API_KEY"); + async fn fetch(url: &str) -> Option { let r = client().get(url).send().await.ok()?; if !r.status().is_success() {