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 ¢s {
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");
}
}