Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-demo/src/osm_tabs.rs

//! **OSM viewer tabs** — wire facett's state-of-the-art **2D** ([`OsmView`]) and
//! **3D** ([`Map3D`]) OSM map viewers into the shared facett-demo deck as real
//! [`Facet`] tabs (NOT standalone bins), pointed at **real GeoParquet**.
//!
//! ## Data source (graceful, in priority order)
//! 1. `$FACETT_OSM_PARQUET` (a `nodes.parquet`) + `$FACETT_OSM_WAYS` (a
//!    `ways.parquet`) — the explicit override (point either lane at Europe).
//! 2. The **default** liechtenstein fixture under
//!    [`default_nodes`] / [`default_ways`] (353k nodes / 41k ways) — the *instant*
//!    demo data, on `/home`, loaded if it exists.
//! 3. A **synthetic** liechtenstein-ish sample (no fixture on disk) — so the demo
//!    always runs, even on a machine with no OSM data.
//!
//! Loading happens **here in the demo**, not in the leaf viewer crates: the
//! parquet/arrow reader lives in `facett-osm`, and we keep `facett-geomap` /
//! `facett-map3d` free of that heavy dependency. The viewers take pre-loaded
//! `(lon, lat)` points + [`Way`]s through their `from_geometry` constructors.
//!
//! Both tabs follow the **facett discovery contract**: `OsmView` and `Map3D` are
//! canonical [`Facet`]s with `caps()`/`state_json()`/`local()`/`remote()`, so the
//! mega-matrix mechanically enumerates them like every other tab.

use facett::Theme;
use facett::geomap::OsmView;
use facett::map::Way;
use facett_map3d::Map3D;

/// Default liechtenstein nodes (points) — the *instant* demo data under `$HOME`
/// (`$HOME/work/osm/liechtenstein/nodes.parquet`).
/// 353k nodes; loaded if present, else the synthetic sample.
pub fn default_nodes() -> String {
    format!("{}/work/osm/liechtenstein/nodes.parquet", std::env::var("HOME").unwrap_or_default())
}
/// Default liechtenstein ways (the vector basemap) — 41k ways
/// (`$HOME/work/osm/liechtenstein/ways.parquet`).
pub fn default_ways() -> String {
    format!("{}/work/osm/liechtenstein/ways.parquet", std::env::var("HOME").unwrap_or_default())
}

/// **Embedded Sigtuna GeoParquet**, baked into the binary via `include_bytes!`.
/// This is the demo's *filesystem-free* real-OSM source — it works in the browser
/// (wasm has no `std::fs`) AND natively when no other fixture is present. Trimmed
/// to just the WKB `geometry` + slimmed `tags` columns and zstd-compressed so the
/// embed stays small. Decoded in-memory via `facett::osm::read_*_bytes`.
///
/// `sigtuna_nodes.parquet` (~0.6 MB) is the dense node point cloud (2D dot map).
pub const SIGTUNA_NODES: &[u8] = include_bytes!("../assets/sigtuna_nodes.parquet");
/// The embedded Sigtuna ways — the **buildings-inclusive** clip from
/// `osm-katana optimize --buildings` (bbox 17.55,59.57–17.85,59.68, ~0.65 MB).
/// Unlike the old named-first embed, this keeps EVERY `building=*` footprint
/// (regardless of name) and preserves the `height` / `building:levels` tags, so
/// the 3D map extrudes a real lit city of prisms at their true metric heights —
/// not floating points. Roads + water + named POIs ride along for the basemap +
/// labels. ~10k buildings / ~11k polygons / ~6k roads. See
/// `.nornir/buildings-embed.md`.
pub const SIGTUNA_WAYS: &[u8] = include_bytes!("../assets/sigtuna_buildings.parquet");

/// Cap on points loaded for the 2D viewer (PointGrid + LOD decimation handle the
/// rest). Liechtenstein's 353k all fit comfortably; for Europe this caps the load.
const MAX_POINTS: usize = 2_000_000;
/// Cap on ways loaded for either viewer (0 = unlimited; we cap so a "load Europe"
/// click on the 458M-way set stays responsive — the LOD index culls the rest).
const MAX_WAYS_2D: usize = 400_000;
/// 3D extrudes every kept way to a prism, so a tighter cap keeps the mesh sane.
const MAX_WAYS_3D: usize = 60_000;

/// Where the loaded geometry came from (for the tab's source label + tests).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OsmSource {
    /// Explicit `$FACETT_OSM_PARQUET` / `$FACETT_OSM_WAYS`.
    Env,
    /// The default liechtenstein fixture on `/home`.
    Liechtenstein,
    /// The **embedded Sigtuna** GeoParquet baked into the binary — the real-OSM
    /// default that needs no filesystem (so the wasm web demo ships real data).
    Sigtuna,
    /// The synthetic in-memory sample (no fixture on disk).
    Synthetic,
    /// A region clipped out of the planet by the **region picker** (osm-katana),
    /// loaded from its GeoParquet cache dir. Carries the region label (`Sverige` …).
    Region(String),
}

impl OsmSource {
    pub fn label(&self) -> &str {
        match self {
            OsmSource::Env => "GeoParquet ($FACETT_OSM_*)",
            OsmSource::Liechtenstein => "liechtenstein (default)",
            OsmSource::Sigtuna => "Sigtuna (embedded)",
            OsmSource::Synthetic => "synthetic sample",
            OsmSource::Region(r) => r,
        }
    }
}

/// Loaded OSM geometry shared by both viewer tabs.
pub struct OsmGeometry {
    pub points: Vec<(f64, f64)>,
    pub ways_2d: Vec<Way>,
    pub ways_3d: Vec<Way>,
    pub source: OsmSource,
}

/// Resolve the paths to read: env override wins; else the liechtenstein default
/// **if both files exist**; else `None` (→ synthetic). Returns `(nodes, ways,
/// source)`.
fn resolve_paths() -> Option<(String, String, OsmSource)> {
    let env_nodes = std::env::var("FACETT_OSM_PARQUET").ok();
    let env_ways = std::env::var("FACETT_OSM_WAYS").ok();
    if env_nodes.is_some() || env_ways.is_some() {
        // Either may be set independently; fill the other from the default.
        let nodes = env_nodes.unwrap_or_else(default_nodes);
        let ways = env_ways.unwrap_or_else(default_ways);
        return Some((nodes, ways, OsmSource::Env));
    }
    let (nodes, ways) = (default_nodes(), default_ways());
    if std::path::Path::new(&nodes).exists() && std::path::Path::new(&ways).exists() {
        return Some((nodes, ways, OsmSource::Liechtenstein));
    }
    None
}

/// Load the OSM geometry the two tabs share. Reads real GeoParquet when a source
/// is available (env or the liechtenstein default), else builds the synthetic
/// sample. Never panics: a read error logs to stderr and falls back to synthetic.
pub fn load() -> OsmGeometry {
    if let Some((nodes_path, ways_path, source)) = resolve_paths() {
        if let Some(geo) = load_from(&nodes_path, &ways_path, source) {
            return geo;
        }
        eprintln!("osm_tabs: no features read from the fixture; falling back to embedded Sigtuna.");
    }
    // No env override / on-disk fixture → use the REAL embedded Sigtuna clip (works
    // natively AND in the browser; no filesystem needed). Synthetic is the last
    // resort if even the embedded blobs fail to decode (should never happen).
    load_sigtuna().unwrap_or_else(synthetic)
}

/// Load the **embedded Sigtuna** GeoParquet ([`SIGTUNA_NODES`] / [`SIGTUNA_WAYS`])
/// straight from the binary — the filesystem-free real-OSM source the wasm web demo
/// ships (and the native default when no `/home` fixture / `$FACETT_OSM_*` override
/// is present). Decodes the embedded bytes in-memory via `facett::osm::read_*_bytes`.
/// `None` only if both blobs fail to decode (so `load` can fall back to synthetic).
pub fn load_sigtuna() -> Option<OsmGeometry> {
    let theme = Theme::default();
    let points = match facett::osm::read_points_bytes(SIGTUNA_NODES, MAX_POINTS) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("osm_tabs: embedded Sigtuna nodes failed to decode: {e}");
            Vec::new()
        }
    };
    let ways_2d = match facett::osm::read_ways_bytes(SIGTUNA_WAYS, MAX_WAYS_2D, &theme) {
        Ok(w) => w,
        Err(e) => {
            eprintln!("osm_tabs: embedded Sigtuna ways failed to decode: {e}");
            Vec::new()
        }
    };
    if points.is_empty() && ways_2d.is_empty() {
        return None;
    }
    let ways_3d: Vec<Way> = ways_2d.iter().take(MAX_WAYS_3D).cloned().collect();
    Some(OsmGeometry { points, ways_2d, ways_3d, source: OsmSource::Sigtuna })
}

/// The `(lon, lat)` centroid of a way's vertices (`None` for an empty way).
fn way_centroid(w: &Way) -> Option<(f64, f64)> {
    if w.lonlat.is_empty() {
        return None;
    }
    let n = w.lonlat.len() as f64;
    let (sl, sa) = w.lonlat.iter().fold((0.0, 0.0), |(sl, sa), &(lon, lat)| (sl + lon, sa + lat));
    Some((sl / n, sa / n))
}

/// **Crop an OSM way set to the dense TOWN CORE.** A regional OSM clip (embedded
/// Sigtuna spans ~16 km) normalises the extruded scene to a design box where real
/// houses are microscopic slivers — you can't SEE THE HOUSES, and the car-nav "street
/// chase" lands hundreds of metres up. This finds the densest **building** cluster
/// (coarse ~`radius_m` grid over building centroids, pick the fullest cell) and keeps
/// every way (buildings + streets) whose centroid lies within `radius_m` of that cell
/// centre, so the returned patch is house-scale: the 3D map normalises it to a box
/// where prisms read as buildings and the [`facett_map3d::SigtunaTour`] drive is a real
/// street chase. Pure + dep-light (planar metric approximation at the patch latitude).
/// Returns the original set unchanged if it holds no buildings.
#[must_use]
pub fn crop_to_town_core(ways: &[Way], radius_m: f64) -> Vec<Way> {
    // Metres-per-degree at the data's latitude (planar approximation over a town).
    let lat0 = ways.iter().filter_map(way_centroid).next().map_or(59.6, |(_, lat)| lat);
    let m_per_deg_lat = 111_320.0_f64;
    let m_per_deg_lon = 111_320.0_f64 * lat0.to_radians().cos();
    let cell_deg_lat = radius_m / m_per_deg_lat;
    let cell_deg_lon = radius_m / m_per_deg_lon.max(1.0);

    // Bin building centroids into ~radius-sized cells; the fullest cell is the core.
    use std::collections::HashMap;
    let mut bins: HashMap<(i64, i64), (usize, f64, f64)> = HashMap::new();
    for w in ways {
        let is_building = w.kind.contains("building") || w.fill.is_some();
        if !is_building {
            continue;
        }
        if let Some((lon, lat)) = way_centroid(w) {
            let key = ((lon / cell_deg_lon).floor() as i64, (lat / cell_deg_lat).floor() as i64);
            let e = bins.entry(key).or_insert((0, 0.0, 0.0));
            e.0 += 1;
            e.1 += lon;
            e.2 += lat;
        }
    }
    let Some((_, &(cnt, sl, sa))) = bins.iter().max_by_key(|(_, v)| v.0) else {
        return ways.to_vec(); // no buildings → nothing to centre on; keep all
    };
    let (cx, cy) = (sl / cnt as f64, sa / cnt as f64);

    // Keep every way whose centroid is within `radius_m` of the core centre.
    let within = |lon: f64, lat: f64| -> bool {
        let dx = (lon - cx) * m_per_deg_lon;
        let dy = (lat - cy) * m_per_deg_lat;
        (dx * dx + dy * dy).sqrt() <= radius_m
    };
    ways.iter()
        .filter(|w| way_centroid(w).is_some_and(|(lon, lat)| within(lon, lat)))
        .cloned()
        .collect()
}

/// Pick a **street route** for the cinematic tour's drive phase: the longest road
/// (line) way in `ways`, downsampled to ≤ 48 waypoints (its own Mercator-unit
/// `points`, which [`Map3D::with_route`] projects onto the extruded streets), and
/// **oriented to head INTO the town core** (not out of it). Empty when the set carries
/// no road ways (the tour then degrades to a low ground-level creep). **The ONE street
/// picker** (L5): the `sigtuna_demo` bin, the OSM-3D tab tour ([`osm3d_view`]), and the
/// tour proof all call this rather than forking a second copy — a road is an open
/// (unfilled, no building height) line way of ≥ 2 points, so the filled building rings
/// are skipped and a real highway is chosen.
///
/// The picked polyline is oriented so the drive **ends nearer the building centroid**
/// than it starts: the reported "the walk drives backwards and out of the town" bug was
/// partly the route being traversed outward. We reverse it when needed so travel runs
/// toward the dense core — down the street, between the houses.
#[must_use]
pub fn street_route(ways: &[Way]) -> Vec<(f32, f32)> {
    let seg_len = |w: &Way| -> f32 {
        w.points
            .windows(2)
            .map(|s| ((s[1].0 - s[0].0).powi(2) + (s[1].1 - s[0].1).powi(2)).sqrt())
            .sum()
    };
    let Some(longest) = ways
        .iter()
        .filter(|w| w.fill.is_none() && w.height_m.is_none() && w.points.len() >= 2)
        .max_by(|a, b| seg_len(a).partial_cmp(&seg_len(b)).unwrap_or(std::cmp::Ordering::Equal))
    else {
        return Vec::new();
    };
    let pts = &longest.points;
    let stride = (pts.len() / 48).max(1);
    let mut wps: Vec<(f32, f32)> = pts.iter().step_by(stride).copied().collect();
    if let Some(&last) = pts.last() && wps.last() != Some(&last) {
        wps.push(last);
    }
    // Orient INWARD: the centroid of the building footprints is the town core; if the
    // route currently ENDS farther from it than it starts, it heads outward → reverse
    // so the drive runs toward the core (down the street into the houses, not away).
    if wps.len() >= 2 {
        let mut n = 0.0f64;
        let (mut cx, mut cy) = (0.0f64, 0.0f64);
        for w in ways.iter().filter(|w| w.fill.is_some() || w.height_m.is_some()) {
            for &(x, y) in &w.points {
                cx += x as f64;
                cy += y as f64;
                n += 1.0;
            }
        }
        if n > 0.0 {
            let (cx, cy) = (cx / n, cy / n);
            let d2 = |p: &(f32, f32)| (p.0 as f64 - cx).powi(2) + (p.1 as f64 - cy).powi(2);
            if d2(wps.first().unwrap()) < d2(wps.last().unwrap()) {
                wps.reverse();
            }
        }
    }
    wps
}

/// Load OSM geometry from an explicit `(nodes.parquet, ways.parquet)` pair, tagging
/// it with `source`. Returns `None` (so the caller can fall back) if both reads
/// produced nothing. This is the runtime-path entry the **region picker** uses to
/// point the viewer at a freshly clipped region — `load()` is the env/default
/// wrapper over it. Never panics: a read error logs to stderr and yields empties.
pub fn load_from(nodes_path: &str, ways_path: &str, source: OsmSource) -> Option<OsmGeometry> {
    let theme = Theme::default();
    let points = match facett::osm::read_points(nodes_path, MAX_POINTS) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("osm_tabs: read_points({nodes_path}) failed: {e}");
            Vec::new()
        }
    };
    // Read ways once at the larger 2D cap, reuse a (cheaper) prefix for 3D.
    let ways_2d = match facett::osm::read_ways(ways_path, MAX_WAYS_2D, &theme) {
        Ok(w) => w,
        Err(e) => {
            eprintln!("osm_tabs: read_ways({ways_path}) failed: {e}");
            Vec::new()
        }
    };
    if points.is_empty() && ways_2d.is_empty() {
        return None;
    }
    let ways_3d: Vec<Way> = ways_2d.iter().take(MAX_WAYS_3D).cloned().collect();
    Some(OsmGeometry { points, ways_2d, ways_3d, source })
}

/// The synthetic fallback: a liechtenstein-ish point cluster + a few typed ways
/// (the same `sample_dataset` the standalone bin uses), so the demo runs with no
/// fixture on disk.
fn synthetic() -> OsmGeometry {
    let (points, ways) = facett::geomap::osm2d::sample_dataset(20_000);
    let ways_3d = ways.clone();
    OsmGeometry { points, ways_2d: ways, ways_3d, source: OsmSource::Synthetic }
}

/// The stable tab title for the 2D OSM viewer (the deck/discovery-contract key —
/// kept constant so the legend + mega-matrix enumerate it; the source rides in
/// `state_json`, not the title).
pub const TITLE_2D: &str = "OSM 2D";
/// The stable tab title for the 3D OSM viewer.
pub const TITLE_3D: &str = "OSM 3D";

/// Build the **2D OSM** tab (a [`OsmView`] [`Facet`]) from loaded geometry. Stable
/// title (`OSM 2D`); the source label is observable in `state_json` / the legend.
///
/// **ONE 2D path, native AND web (unification / dedup):** the 2D basemap draws
/// through [`OsmView`]'s CPU f64 [`MercatorCamera`] painter on *both* the native
/// window and the wasm/WebGL demo — the SAME code, so a fix lands once and shows
/// everywhere. The view is deliberately NOT opted into the `map-wgpu` GPU line-ways
/// kernel here: that native-only fast-path (`with_gpu(true)`) had diverged from the
/// CPU basemap — buggy zoom (f32 cancellation in the GPU `MapTransform` at high zoom)
/// and dropped streets (the GPU compute-cull LOD differs from `query_visible`) — so
/// the desktop 2D was stale while the web 2D (CPU) stayed correct. Sharing the CPU
/// painter brings the desktop up to the web-correct render (full streets + working
/// zoom) and removes the fork. The demo's OSM datasets are LOD-culled + point-budgeted,
/// so the CPU painter is comfortably real-time at this scale.
pub fn osm2d_view(geo: &OsmGeometry) -> OsmView {
    // The **Garmin-nav look**: the fixed cartographic *day* preset (a real map's
    // paper/water/green + an orange→yellow→white road ramp) turns on the road-class
    // hierarchy, road casing, one-way direction chevrons and priority road-name
    // labels in `OsmView`'s painter — the themed look drew flat coloured lines. A
    // bright cased **route highlight** (the "you are going here" line) is threaded
    // down the longest street so the routed-path layer is visible on first load.
    let route = street_route_ll(&geo.ways_2d);
    let view = OsmView::from_geometry(TITLE_2D, geo.points.clone(), geo.ways_2d.clone())
        .cartographic(true)
        .with_route(route);
    // ── OPT INTO THE GPU LINE LANE ────────────────────────────────────────────
    // `main.rs` installs the `GpuMapRenderer` into the eframe RenderState, and
    // `OsmView` carries the whole wgpu compute-cull + draw_indirect path — but the
    // pane is opt-IN and nobody ever opted it in. The demo's `map` tab did
    // (`.with_gpu(true)` in lib.rs); the OSM 2D tab did not, so it CPU-tessellated
    // every one of Sigtuna's 41702 ways through the egui painter on one core, every
    // frame. Measured on oden with the RTX 4090 correctly selected: `gpu_enabled =
    // false`, 4.24 fps (release) / 1.89 fps (debug) — the "OSM 2D is dead slow"
    // report, with the GPU sitting idle. The lane is fail-safe: without the feature,
    // without a RenderState, or under a 2.5D tilt, `draw_ways_gpu` returns false and
    // the CPU painter still draws everything.
    #[cfg(feature = "map-wgpu")]
    let view = view.with_gpu(true);
    view
}

/// The longest **road** in `ways`, decimated to a handful of `(lon,lat)` waypoints
/// — the 2D route-highlight source (the 2D analogue of [`street_route`], which
/// returns Mercator points for the 3D ribbon). Empty when there is no road way.
fn street_route_ll(ways: &[Way]) -> Vec<(f64, f64)> {
    let seg_len = |w: &Way| -> f64 {
        w.lonlat.windows(2).map(|s| ((s[1].0 - s[0].0).powi(2) + (s[1].1 - s[0].1).powi(2)).sqrt()).sum()
    };
    let Some(longest) = ways
        .iter()
        .filter(|w| w.kind == "road" && w.lonlat.len() >= 2)
        .max_by(|a, b| seg_len(a).partial_cmp(&seg_len(b)).unwrap_or(std::cmp::Ordering::Equal))
    else {
        return Vec::new();
    };
    let pts = &longest.lonlat;
    let stride = (pts.len() / 48).max(1);
    let mut wps: Vec<(f64, f64)> = pts.iter().step_by(stride).copied().collect();
    if let Some(&last) = pts.last() && wps.last() != Some(&last) {
        wps.push(last);
    }
    wps
}

/// Build the **3D OSM** tab (a [`Map3D`] [`Facet`]) from loaded geometry. Extrudes
/// the way set into a lit, navigable 3D city. When the synthetic fallback is used
/// (no real footprints), fall back to the extruded znippy-rabbit skyline so the
/// pane is never empty.
pub fn osm3d_view(geo: &OsmGeometry) -> Map3D {
    if geo.source == OsmSource::Synthetic && geo.ways_3d.iter().all(|w| w.fill.is_none()) {
        // The synthetic sample's ways are lines (a motorway/coastline) → nothing
        // to extrude into prisms; show the rabbit skyline instead.
        // Auto-play the cinematic flythrough so the tab flies itself (the OSM-3D
        // flight-sim auto-pilot) rather than sitting on a static orbit.
        let m = Map3D::synthetic(TITLE_3D).with_flythrough(true);
        #[cfg(feature = "map3d-wgpu")]
        let m = m.with_gpu(true);
        return m;
    }
    // Real city: extrude + frame the densest building cluster so the skyline reads
    // (at country scale individual footprints are sub-pixel). `from_geometry`
    // dollies the camera into the dense cluster by default.
    //
    // Sigtuna sits on Lake Mälaren (Sigtunafjärden), but the building extract carries
    // no `natural=water` polygon — so the reflective-water lane (Gerstner + Fresnel +
    // depth foam + planar reflection, the map3d flight-sim Phase-2 work) never lit and
    // the 3D tab read like a dry city, indistinguishable from plain OSM 3D. Seed the
    // bay along the town's south shore as a water body so the flight-sim water renders
    // against the REAL skyline, and force the water tier on (it is otherwise gated to a
    // < 300 m camera altitude, above which the dense-cluster framing sits).
    // Crop the (possibly regional) clip to its dense TOWN CORE so the extruded scene is
    // HOUSE-SCALE — otherwise whole-clip normalisation shrinks real houses to invisible
    // slivers AND, crucially, the huge metres-per-design-unit pushes the camera's hard
    // min-distance floor hundreds of metres up, so the tour's street chase can never get
    // DOWN among the houses (it lands ~480 m over the road on the raw 16 km clip). This is
    // the SAME house-scale crop the `sigtuna_demo` bin flies (L5: shared crop + tour), and
    // it is what lets the drive phase reach a real ~30 m street-level distance.
    let core = crop_to_town_core(&geo.ways_3d, 400.0);
    let ways: Vec<Way> = if core.len() >= 20 { core } else { geo.ways_3d.clone() };
    // Real DEM/relief terrain under the city (#24a): the orbit map3d is no longer a
    // flat plane. Sample a real SRTM `.hgt` if `$FACETT_HGT` points at one, else the
    // deterministic procedural fallback (so a fixture-free run still shows topography).
    let (elev, _real) = facett_map3d::TerrainField::from_env();
    // The longest real street in the core → the tour's drive-phase route (the shared
    // street picker, L5 — the same one the `sigtuna_demo` bin flies).
    let wps = street_route(&ways);
    // **Real extruded, roofed, COLOURED houses** — the SAME construction the
    // `sigtuna_demo` bin flies (L5, no degraded twin). The tab used to build the plain
    // `from_geometry_relief` (flat-capped, monochrome) path, so facett-demo's 3D city
    // read as flat grey footprints next to sigtuna_demo's pitched, tinted rooftops —
    // the reported "houses are FLAT, total misery vs the single window". `..._roofed`
    // grows gabled/hipped roofs (OSM `roof:shape`) + OSM colours, and `with_roof_accent`
    // lifts the roof caps to a warm highlight (Google-Maps-3D massing).
    let m = Map3D::from_geometry_relief_roofed(TITLE_3D, &ways, &elev, 96, 3.0)
        .with_roof_accent(0.55)
        .with_minimap(true)
        // Garmin nav chrome (camera-only, no extra data): a north rose that swings with
        // the bearing + a dynamic metres/km scale bar sized to the live zoom.
        .with_compass(true)
        .with_scale_bar(true)
        // The cinematic **Sigtuna TOUR** (shared, L5): start HIGH circling the town so you
        // SEE THE HOUSES from the air → DESCEND → DRIVE the streets at real STREET LEVEL (a
        // ~30 m car-navigator distance, held via the scene scale). This replaces the framed
        // **flythrough**, which orbited the city high and never went DOWN TO STREET LEVEL —
        // the exact gap the user reported vs `sigtuna_demo`. `with_route` first so the drive
        // phase has a street to follow (else it degrades to a low ground-level creep).
        .with_route(&ways, &wps)
        .with_tour(true);
    #[cfg(feature = "map3d-wgpu")]
    let m = m.with_gpu(true);
    m
}

#[cfg(test)]
mod tests {
    use super::*;
    use facett::Facet;
    use facett::core::harness;

    /// **INJECT-ASSERT (crop finds the town core):** the embedded Sigtuna clip spans
    /// ~16 km; cropping to a 400 m core keeps a real, much smaller, non-empty patch of
    /// ways all within ~2·radius of one another — the house-scale scene the navigator
    /// tour flies (so the extruded houses are visible, not regional slivers).
    #[test]
    fn crop_to_town_core_narrows_to_a_house_scale_patch() {
        let geo = load_sigtuna().expect("embedded Sigtuna decodes");
        let core = crop_to_town_core(&geo.ways_3d, 400.0);
        assert!(!core.is_empty(), "the core keeps ways");
        assert!(core.len() < geo.ways_3d.len(), "the crop narrows the regional clip ({} < {})", core.len(), geo.ways_3d.len());
        assert!(core.len() > 20, "the Sigtuna core is a real dense patch, got {}", core.len());
        let cents: Vec<(f64, f64)> = core.iter().filter_map(way_centroid).collect();
        let (mut minlon, mut maxlon, mut minlat, mut maxlat) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
        for (lon, lat) in &cents {
            minlon = minlon.min(*lon);
            maxlon = maxlon.max(*lon);
            minlat = minlat.min(*lat);
            maxlat = maxlat.max(*lat);
        }
        // ~2·400 m in degrees at Sigtuna's latitude (lon ~0.014°, lat ~0.0072°) + slack.
        assert!(maxlon - minlon < 0.02, "core is compact in lon ({:.4}°)", maxlon - minlon);
        assert!(maxlat - minlat < 0.02, "core is compact in lat ({:.4}°)", maxlat - minlat);
    }

    /// The **embedded Sigtuna** GeoParquet (the wasm web demo's real-OSM source,
    /// `include_bytes!`d into the binary) decodes in-memory — no filesystem — into a
    /// real point cloud + vector basemap, and both viewer tabs render it. This is
    /// the inject-assert proof the wasm demo ships REAL Sigtuna data (not synthetic):
    /// the bytes are decoded the same way the browser will, with the coordinates
    /// landing inside the Sigtuna bbox.
    #[test]
    fn embedded_sigtuna_decodes_and_builds_both_viewers() {
        let geo = load_sigtuna().expect("embedded Sigtuna decodes");
        assert_eq!(geo.source, OsmSource::Sigtuna);
        assert!(geo.points.len() > 10_000, "Sigtuna clip has ~40k node points, got {}", geo.points.len());
        assert!(geo.ways_2d.len() > 1_000, "Sigtuna clip has ~18k ways, got {}", geo.ways_2d.len());
        // Every point sits in the Sigtuna bbox (17.55..17.85 E, 59.57..59.68 N).
        let (lon, lat) = geo.points[0];
        assert!((17.5..17.9).contains(&lon) && (59.5..59.7).contains(&lat), "in Sigtuna: {lon},{lat}");

        let mut v2 = osm2d_view(&geo);
        let r2 = harness::headless_render(&mut v2);
        assert!(r2.drew(), "the 2D tab tessellates the real Sigtuna points");
        assert!(r2.state["points"].as_u64().unwrap() > 10_000);

        let mut v3 = osm3d_view(&geo);
        let r3 = harness::headless_render(&mut v3);
        assert!(r3.drew(), "the 3D tab extrudes the real Sigtuna footprints");
        assert!(r3.state["triangles"].as_u64().unwrap() > 0);
    }

    /// ★ **RED-when-broken: a TAGLESS ways source reaches the deck as OVERPRINT, and
    /// an OSM cut never does** (lane TL, 2026-09-03).
    ///
    /// LAW #2: the tagless-source seam is a facett change, so the demo has to SHOW it.
    /// `$FACETT_OSM_WAYS` is the one door an operator points at their own data, and a
    /// Trafikverket / NVDB export is a plain WKB table with no `tags` column. Since lane
    /// SI such a file read **zero** ways and the tab was silently empty; it now reads and
    /// paints in `facett_map::untagged_source_style`'s overprint ink, and the count is on
    /// the deck at `/deck/facets/OSM 2D/overprint_ways` where the robot scenario asserts
    /// it (`robot.rs`, "an OSM cut carries NO overprint ways").
    ///
    /// **RED ARM**: delete the `let forced = …` line in
    /// `facett-osm/src/lib.rs::read_ways_reported_from` and the first half reads 0 ways.
    /// Make it unconditional and the embedded-Sigtuna half reports a non-zero overprint
    /// count — the whole basemap painted magenta.
    #[test]
    fn a_tagless_ways_source_reaches_the_deck_as_overprint() {
        use arrow_array::{ArrayRef, BinaryArray, RecordBatch};
        use arrow_schema::{DataType, Field, Schema};
        use std::sync::Arc;

        // A `geometry` column and nothing else — what a non-OSM roads export IS.
        let blobs: Vec<Vec<u8>> = (0..8)
            .map(|i| {
                let (lon, lat): (f64, f64) = (17.60 + f64::from(i) * 0.01, 59.60);
                let mut b = vec![1u8];
                b.extend_from_slice(&2u32.to_le_bytes());
                b.extend_from_slice(&2u32.to_le_bytes());
                for (x, y) in [(lon, lat), (lon + 0.005, lat + 0.005)] {
                    b.extend_from_slice(&x.to_le_bytes());
                    b.extend_from_slice(&y.to_le_bytes());
                }
                b
            })
            .collect();
        let schema = Arc::new(Schema::new(vec![Field::new("geometry", DataType::Binary, false)]));
        let arr = BinaryArray::from(blobs.iter().map(|b| Some(b.as_slice())).collect::<Vec<_>>());
        let batch = RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(arr) as ArrayRef]).unwrap();
        let path = std::env::temp_dir().join(format!("facett_demo_tagless_{}.parquet", std::process::id()));
        {
            let f = std::fs::File::create(&path).unwrap();
            let mut w = parquet::arrow::ArrowWriter::try_new(f, schema, None).unwrap();
            w.write(&batch).unwrap();
            w.close().unwrap();
        }
        let pal = facett_map::MapPalette::cartographic(true);
        let ways = facett::osm::read_ways_with(path.to_str().unwrap(), 0, &pal)
            .expect("a source the operator named is a readable source");
        let _ = std::fs::remove_file(&path);
        assert_eq!(ways.len(), 8, "a tagless source must READ — it read {}", ways.len());

        let geo = OsmGeometry {
            points: Vec::new(),
            ways_2d: ways.clone(),
            ways_3d: ways,
            source: OsmSource::Env,
        };
        let mut v2 = osm2d_view(&geo);
        let r2 = harness::headless_render(&mut v2);
        assert!(r2.drew(), "the deck's OSM 2D tab draws the operator's tagless source");
        assert_eq!(
            r2.state["overprint_ways"].as_u64().unwrap_or_default(),
            8,
            "…and the deck SAYS so, so an empty overlay can never look like a loaded one: {}",
            r2.state["overprint_ways"],
        );

        // The other half: a real OSM cut is NEVER overprint. This is what the robot
        // scenario's `expected: 0` step is holding.
        let osm = load_sigtuna().expect("embedded Sigtuna decodes");
        let mut v_osm = osm2d_view(&osm);
        let r_osm = harness::headless_render(&mut v_osm);
        assert_eq!(
            r_osm.state["overprint_ways"].as_u64().unwrap_or_default(),
            0,
            "an OSM cut carries tags and is classified; a non-zero count here means the \
             whole basemap was painted magenta",
        );
    }

    /// **1e — the OSM-3D tab renders REAL car-navigator streets, not synthetic
    /// ones.** The embedded Sigtuna `ways_2d`/`ways_3d` carry actual `highway=*`
    /// road ways decoded from the parquet (not the rabbit-skyline synthetic
    /// fallback), a real subset of them are NAMED (a genuine OSM `name` tag), and
    /// building the 3D tab from that data produces a non-empty extruded scene
    /// (roads decode + build into the osm3d scene) WITH at least one street-label
    /// anchor surfaced in `state_json` — proving the label pipeline actually ran
    /// on real data, not just that the mesh has triangles.
    #[test]
    fn embedded_sigtuna_roads_are_real_and_named_and_label_the_3d_scene() {
        let geo = load_sigtuna().expect("embedded Sigtuna decodes");
        assert_eq!(geo.source, OsmSource::Sigtuna);

        let roads: Vec<_> = geo.ways_2d.iter().filter(|w| w.kind == "road").collect();
        assert!(!roads.is_empty(), "the Sigtuna clip carries real highway=* road ways");
        let named = roads.iter().filter(|w| !w.name.is_empty()).count();
        assert!(named > 0, "at least some real Sigtuna roads carry an OSM `name` tag, got 0 of {}", roads.len());

        // The demo TAB (`osm3d_view`) now crops to the dense TOWN CORE for the cinematic
        // street-level tour (so the camera can descend among the houses) — the core is a
        // building cluster whose local roads may be unnamed, so we assert the LABEL
        // PIPELINE on the FULL clip (which carries the named streets proven above), built
        // the SAME way the tab extrudes its mesh. This keeps the test about "named roads
        // label the 3D scene" and not about which patch the tour happens to crop.
        let (elev, _real) = facett_map3d::TerrainField::from_env();
        let mut v3 = Map3D::from_geometry_relief(TITLE_3D, &geo.ways_3d, &elev, 48, 3.5);
        let r3 = harness::headless_render(&mut v3);
        assert!(r3.drew(), "the 3D scene extrudes the real Sigtuna street network");
        assert!(r3.state["triangles"].as_u64().unwrap() > 0, "the road ways decode + build into the osm3d scene");
        assert!(
            r3.state["street_labels"].as_u64().unwrap() > 0,
            "the real named roads produced street-label anchors: {}",
            r3.state
        );

        // And the shipped tab itself still extrudes a real, non-empty city from the core.
        let mut tab = osm3d_view(&geo);
        let rtab = harness::headless_render(&mut tab);
        assert!(rtab.drew() && rtab.state["triangles"].as_u64().unwrap() > 0, "the cropped tour tab extrudes a city");
    }

    #[test]
    fn synthetic_load_builds_both_viewers_non_empty() {
        // No env, no fixture needed: the synthetic lane builds both tabs with real
        // geometry. Inject (the synthetic dataset) → assert both render.
        let geo = synthetic();
        assert_eq!(geo.source, OsmSource::Synthetic);
        assert!(!geo.points.is_empty(), "synthetic has points");

        let mut v2 = osm2d_view(&geo);
        let r2 = harness::headless_render(&mut v2);
        assert!(r2.drew(), "the 2D tab tessellates to vertices");
        assert!(r2.state["points"].as_u64().unwrap() > 0);

        let mut v3 = osm3d_view(&geo);
        let r3 = harness::headless_render(&mut v3);
        assert!(r3.drew(), "the 3D tab tessellates to vertices");
        assert!(r3.state["triangles"].as_u64().unwrap() > 0);
    }

    #[test]
    fn both_tabs_are_navigable_facets() {
        let geo = synthetic();
        assert!(osm2d_view(&geo).caps().navigable, "2D OSM is navigable");
        assert!(osm3d_view(&geo).caps().navigable, "3D OSM is navigable");
    }
}