From 173f6f7282360f208661443c493ebd01fbdbe5b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 13:52:45 +0000 Subject: [PATCH 1/2] =?UTF-8?q?cockpit-server:=20/osm=20dual=20basemap=20?= =?UTF-8?q?=E2=80=94=20OSM=20map=20<->=20ESRI=20satellite=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /osm slippy cockpit can now switch skins: OSM raster tiles <-> ESRI World Imagery satellite — the SAME keyless imagery source the ver-9 terrain skins (canyon/havel) drape from, so the flat map and the 3D scenes share one imagery truth. Same slippy addresses, same HHTL keys — two skins of one cascade. osm_tiles.rs: SAT_TILE_URL const + sat_tile_url() — ESRI's path is z/y/x (row before column), unlike OSM's z/x/y; a shared fill_template carries the axis order in the template so both fill through one call. The /api/osm/locate and /api/osm/tile/:z/:x/:y responses now report sat_tile_url + sat_source alongside the OSM fields (additive, non- breaking). New test pins the y-before-x swap. osm.rs (the inline page): a BASEMAPS table (src template, attribution, next), a `sat`/`osm` toggle in the map controls (button names the skin you will switch TO), attribution follows the active source (OSM contributors <-> Esri, Maxar, Earthstar Geographics). Verified headless on the extracted page HTML: OSM->sat->OSM round trip, tile srcs swap with the z/y/x order correct (12/2197/1340 <-> 12/1340/2197), attribution swaps. cockpit-server itself cannot compile in this container (rusty_v8 static-lib download blocked by org egress policy); the Rust side mirrors the existing tested handler pattern and CI runs the new test. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC --- .../2026-07-10-garmin-dem-satellite-skin.md | 17 +++++++ crates/cockpit-server/src/osm.rs | 27 +++++++++-- crates/cockpit-server/src/osm_tiles.rs | 46 +++++++++++++++++-- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md b/claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md index 4fbd2a89a..139a7b107 100644 --- a/claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md +++ b/claude-notes/plans/2026-07-10-garmin-dem-satellite-skin.md @@ -80,3 +80,20 @@ the render on mobile; the full grid is the desktop birdview. both reuse the DEM+skin pipeline; Havel is flat lowland → skin-dominant. - **Sentinel-2 skin** — swap the ESRI source for `GrandCanyon_S2_20260620.tif` (source-agnostic; the skin just comes from the demgrid rgb). + +## /osm dual basemap (2026-07-10, follow-up shipped) +- The `/osm` slippy cockpit gets a **basemap toggle**: OSM map ↔ ESRI World Imagery + satellite — the SAME keyless imagery the ver-9 terrain skins drape from, so the flat + map and the 3D scenes share one imagery truth. Same tile addresses, same HHTL keys — + two skins. +- Server (`osm_tiles.rs`): `SAT_TILE_URL` + `sat_tile_url()` (ESRI is **z/y/x** — row + before column; the shared `fill_template` encodes the swap), both locate/tile-meta + responses now report `sat_tile_url`/`sat_source` (additive JSON). Test asserts the + axis order. +- Page (`osm.rs`): `BASEMAPS` table (src template + attribution + next), `sat`/`osm` + toggle button in the map controls, attribution swaps with the source (OSM + contributors ↔ Esri/Maxar/Earthstar). +- Verified headless on the extracted page: toggle OSM→sat→OSM, srcs swap with the + z/y/x order CORRECT, attribution follows. cockpit-server itself cannot compile in + this container (rusty_v8 static-lib download blocked by egress policy) — Rust side + mirrors the existing tested pattern; CI runs the tests. diff --git a/crates/cockpit-server/src/osm.rs b/crates/cockpit-server/src/osm.rs index e5cc03391..ec0ad3478 100644 --- a/crates/cockpit-server/src/osm.rs +++ b/crates/cockpit-server/src/osm.rs @@ -57,9 +57,9 @@ const PAGE: &str = r##"
-
+
drag to pan · click a point for its HHTL key
-
© OpenStreetMap contributors
+
© OpenStreetMap contributors

OSM cockpit

@@ -91,6 +91,19 @@ const map=document.getElementById('map'), tilesEl=document.getElementById('tiles let z=12; // Berlin-ish default view let cx=lon2x(13.404954,z), cy=lat2y(52.520008,z); // center in fractional tile units +// ── dual map: OSM (z/x/y) ↔ ESRI World Imagery satellite (z/y/x — row first!). +// Same slippy addresses, same HHTL keys — two skins. Mirrors cockpit-server:: +// osm_tiles::{OSM_TILE_URL, SAT_TILE_URL}; the locate API reports both URLs. +const BASEMAPS={ + osm:{ src:(z,x,y)=>`https://tile.openstreetmap.org/${z}/${x}/${y}.png`, + attr:'© OpenStreetMap contributors', + next:'sat' }, + sat:{ src:(z,x,y)=>`https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${z}/${y}/${x}`, + attr:'Powered by Esri — Source: Esri, Maxar, Earthstar Geographics', + next:'osm' }, +}; +let basemap='osm'; + function render(){ const w=map.clientWidth, h=map.clientHeight, n=Math.pow(2,z); tilesEl.innerHTML=''; @@ -99,10 +112,11 @@ function render(){ tilesEl.style.transform=`translate(${ox}px,${oy}px)`; const x0=Math.floor(cx - w/512)-1, x1=Math.floor(cx + w/512)+1; const y0=Math.floor(cy - h/512)-1, y1=Math.floor(cy + h/512)+1; + const bm=BASEMAPS[basemap]; for(let ty=y0;ty<=y1;ty++) for(let tx=x0;tx<=x1;tx++){ const wx=((tx%n)+n)%n; if(ty<0||ty>=n) continue; const img=new Image(); - img.src=`https://tile.openstreetmap.org/${z}/${wx}/${ty}.png`; + img.src=bm.src(z,wx,ty); img.style.left=(tx*256)+'px'; img.style.top=(ty*256)+'px'; tilesEl.appendChild(img); } @@ -123,6 +137,13 @@ function zoom(d){ const nz=Math.max(0,Math.min(19,z+d)); if(nz===z) return; const f=Math.pow(2,nz-z); cx*=f; cy*=f; z=nz; render(); } document.getElementById('zin').onclick=e=>{ e.stopPropagation(); zoom(1); }; document.getElementById('zout').onclick=e=>{ e.stopPropagation(); zoom(-1); }; +// basemap toggle — the button names the OTHER skin (what you'll switch TO); +// attribution follows the active source (OSM contributors ↔ Esri). +document.getElementById('base').onclick=e=>{ e.stopPropagation(); + basemap=BASEMAPS[basemap].next; + e.target.textContent=BASEMAPS[basemap].next; + document.getElementById('attr').innerHTML=BASEMAPS[basemap].attr; + render(); }; map.addEventListener('wheel',e=>{ e.preventDefault(); zoom(e.deltaY<0?1:-1); },{passive:false}); // ── click → server-side HHTL key ── diff --git a/crates/cockpit-server/src/osm_tiles.rs b/crates/cockpit-server/src/osm_tiles.rs index a828e2002..c061f2283 100644 --- a/crates/cockpit-server/src/osm_tiles.rs +++ b/crates/cockpit-server/src/osm_tiles.rs @@ -27,18 +27,40 @@ use std::f64::consts::PI; /// by the client; the cockpit only computes the address. pub const OSM_TILE_URL: &str = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"; +/// The satellite basemap source — ESRI World Imagery, the SAME keyless slippy +/// grid the DEM/skin bakes drape from (`scripts/fetch_iceland_dem.py`), so the +/// dual map (OSM ↔ satellite) and the ver-9 terrain skins share one imagery +/// truth. NOTE the path order: ESRI is **z/y/x** (row before column), unlike +/// the OSM z/x/y — the template placeholders encode that swap. +pub const SAT_TILE_URL: &str = + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"; + /// Native HHTL depth: 3 tiers × 8 levels = 24 quadtree levels (zoom 0..=24). pub const HHTL_DEPTH: u32 = 24; -/// The concrete tile-source URL for a `z/x/y` address. -#[must_use] -pub fn tile_url(z: u32, x: u32, y: u32) -> String { - OSM_TILE_URL +/// Fill a slippy-tile URL template (`{z}`/`{x}`/`{y}`) for an address. The +/// template carries the axis order, so OSM (z/x/y) and ESRI (z/y/x) both fill +/// correctly through the same call. +fn fill_template(template: &str, z: u32, x: u32, y: u32) -> String { + template .replace("{z}", &z.to_string()) .replace("{x}", &x.to_string()) .replace("{y}", &y.to_string()) } +/// The concrete OSM tile-source URL for a `z/x/y` address. +#[must_use] +pub fn tile_url(z: u32, x: u32, y: u32) -> String { + fill_template(OSM_TILE_URL, z, x, y) +} + +/// The concrete SATELLITE (ESRI World Imagery) tile URL for the same `z/x/y` +/// address — the dual-map partner of [`tile_url`]. +#[must_use] +pub fn sat_tile_url(z: u32, x: u32, y: u32) -> String { + fill_template(SAT_TILE_URL, z, x, y) +} + /// WebMercator (EPSG:3857) forward: geographic `(lon, lat)` → slippy tile /// `(x, y)` at zoom `z`. `x` grows east, `y` grows south (TMS-flipped is the /// caller's concern). Clamped to the valid `0..2^z` range. @@ -151,6 +173,10 @@ pub async fn osm_locate_handler(Query(q): Query) -> Json Date: Fri, 10 Jul 2026 14:04:12 +0000 Subject: [PATCH 2/2] cockpit-server: /osm tile-source panel follows the active basemap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit finding on #101: the click->locate panel always displayed d.tile_url, so on the satellite basemap it showed the OSM URL — a mismatch with the visible tiles. Select d.sat_tile_url when the sat skin is active (the locate API already reports both). Verified headless with a stubbed locate API: click on OSM shows the openstreetmap URL, toggle to sat + click shows the arcgisonline URL. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012jEwwaT5JZ5x8qWvcnaMYC --- crates/cockpit-server/src/osm.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/cockpit-server/src/osm.rs b/crates/cockpit-server/src/osm.rs index ec0ad3478..bc7e4ae88 100644 --- a/crates/cockpit-server/src/osm.rs +++ b/crates/cockpit-server/src/osm.rs @@ -165,7 +165,9 @@ map.addEventListener('click',async e=>{ document.getElementById('heel').textContent='0x'+d.hhtl.heel.toString(16).padStart(4,'0'); document.getElementById('hip').textContent='0x'+d.hhtl.hip.toString(16).padStart(4,'0'); document.getElementById('twig').textContent='0x'+d.hhtl.twig.toString(16).padStart(4,'0'); - document.getElementById('src').textContent=d.tile_url; + // tile source follows the ACTIVE basemap (the locate API reports both URLs) — + // on satellite, showing the OSM URL would mismatch the visible tiles. + document.getElementById('src').textContent=basemap==='sat'?d.sat_tile_url:d.tile_url; }catch(err){ document.getElementById('src').textContent='locate failed: '+err; } });