facett-geomap/tests/osm_point_lod.rs
//! **The BUILDING-VERTEX LOD** — "the corners of the houses are blue, it makes it
//! look like a sea when zooming out".
//!
//! The OSM point layer is the node cloud, and in a real extract most nodes are
//! building corners. It used to paint every surviving node as a circle at *any*
//! zoom, in the **theme's** `point` hue — an unrelated blue on the cartographic-day
//! paper. Zoomed out over a town that is tens of thousands of dots on a pane with
//! hundreds of thousands of pixels: a solid wash of colour, not a set of markers.
//!
//! Three separate things had to be true, and each gets its own oracle here:
//!
//! 1. **The corners must not render when zoomed out at all** (the actual bug — an
//! LOD rule, not a colour choice). `zoomed_out_paints_zero_building_vertices` is
//! the RED-when-broken assertion: at a whole-dataset camera over a dense extract
//! the count of corner-point primitives is **exactly 0**. The pre-fix painter
//! emitted tens of thousands there, so it fails this outright.
//! 2. **When they DO show they are the building outline's own ink**, not blue —
//! `visible_corners_are_the_building_stroke_colour`.
//! 3. **A debug mode makes them deliberately conspicuous**, default OFF, reachable
//! from the UI — `debug_mode_*`.
//!
//! Plus the guard that keeps the cure from being worse than the disease: a SPARSE
//! layer (the demo's "collector data layers" pane, 43 points) must keep every point
//! at every zoom — `a_sparse_layer_is_never_culled_at_any_zoom`.
use facett_core::harness;
use facett_geomap::osm2d::{Msg, OsmView};
/// A pane the size of the demo's map area.
fn rect() -> egui::Rect {
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))
}
/// A **dense** town-scale node cloud — the shape of a real OSM extract, where the
/// overwhelming majority of nodes are building corners. Deterministic (xorshift, no
/// wall clock) so the oracle is reproducible.
fn dense_town(n: usize) -> Vec<(f64, f64)> {
let (lon0, lon1, lat0, lat1) = (17.70, 17.75, 59.60, 59.64);
let mut state: u64 = 0x2545_F491_4F6C_DD1D;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state >> 11) as f64 / (1u64 << 53) as f64
};
(0..n).map(|_| (lon0 + next() * (lon1 - lon0), lat0 + next() * (lat1 - lat0))).collect()
}
/// **THE RED-WHEN-BROKEN ORACLE.** At a zoomed-out (whole-dataset) camera over a
/// dense extract the painter must emit **ZERO** building-vertex primitives.
///
/// This is stated as a count, not as a colour or a stride, because that is what the
/// bug was: the layer *rendered*, and no hue would have saved it. The pre-fix
/// painter drew `visible / stride` circles here — a five-figure number — so it fails
/// this assertion by five orders of magnitude.
#[test]
fn zoomed_out_paints_zero_building_vertices() {
let mut v = OsmView::new("sea", dense_town(40_000), vec![]);
let r = rect();
v.fit_to(r);
let res = harness::headless_render(&mut v);
let vis = res.state["visible_points"].as_u64().unwrap();
let drawn = res.state["drawn_points"].as_u64().unwrap();
assert!(vis > 10_000, "the whole dense cloud IS in view ({vis}) — this is the bug's input");
assert_eq!(drawn, 0, "a zoomed-out frame paints NO corner points, got {drawn}");
assert_eq!(res.state["points_culled"], true, "and reports the cull as data");
// The decision is reproducible from state alone: the ink the layer WOULD have
// painted, against the budget it was measured against.
let ink = res.state["point_ink_frac"].as_f64().unwrap();
let budget = res.state["max_point_ink_frac"].as_f64().unwrap();
assert!(ink > budget, "the cull fired on ink {ink} over budget {budget}");
// …and it was not a marginal call: this frame wanted MORE THAN A SCREENFUL of
// dots. That is the "sea", quantified.
assert!(ink > 1.0, "the pre-fix frame painted over a full screen of dots (ink {ink})");
}
/// The other half of the LOD: **at building zoom the corners come back**, and they
/// are individually meaningful (few enough to read as markers, not as a texture).
#[test]
fn building_zoom_paints_the_corner_points() {
let mut v = OsmView::new("close", dense_town(40_000), vec![]);
let r = rect();
v.fit_to(r);
// Zoom in toward a building-scale camera until the layer is admitted. Each step
// is a real `Msg` through the single update path (live == headless).
let mut drawn = 0u64;
let mut steps = 0;
for _ in 0..12 {
v.update(Msg::ZoomTo { k: 2.0, anchor: r.center(), rect: r });
while v.camera_mut().tick() {}
let res = harness::headless_render(&mut v);
steps += 1;
drawn = res.state["drawn_points"].as_u64().unwrap();
if drawn > 0 {
assert_eq!(res.state["points_culled"], false, "admitted ⇒ not culled");
let ink = res.state["point_ink_frac"].as_f64().unwrap();
let budget = res.state["max_point_ink_frac"].as_f64().unwrap();
assert!(ink <= budget, "admitted ⇒ within the ink budget ({ink} <= {budget})");
break;
}
}
assert!(drawn > 0, "the corner layer is admitted at a close enough zoom (gave up after {steps})");
// The knee is a ZOOM knee, not a "never draws": it took only a handful of 2x
// steps off the fit, i.e. the layer returns at a normal street/building camera
// rather than at some unreachable extreme.
assert!(steps <= 8, "the layer returns at a reachable zoom, took {steps} doublings");
}
/// A **SPARSE** annotation layer (the demo's "collector data layers" pane holds 43
/// points) must never be culled — at any zoom, including the most zoomed-out one.
/// Tens of dots are nowhere near a screenful of ink, so the density rule leaves them
/// alone; a plain "metres per pixel" threshold would have blanked this pane.
#[test]
fn a_sparse_layer_is_never_culled_at_any_zoom() {
let pts: Vec<(f64, f64)> =
(0..43).map(|i| (17.70 + (i % 7) as f64 * 0.004, 59.60 + (i / 7) as f64 * 0.004)).collect();
let mut v = OsmView::new("layers", pts.clone(), vec![]);
let r = rect();
v.fit_to(r);
// The fit itself.
let res = harness::headless_render(&mut v);
assert_eq!(res.state["points_culled"], false, "43 points are not a sea");
assert_eq!(
res.state["drawn_points"].as_u64().unwrap(),
res.state["visible_points"].as_u64().unwrap(),
"every visible point of a sparse layer draws"
);
// …and at every zoom-out step down to the world view.
for _ in 0..6 {
v.update(Msg::ZoomTo { k: 0.25, anchor: r.center(), rect: r });
while v.camera_mut().tick() {}
let res = harness::headless_render(&mut v);
let vis = res.state["visible_points"].as_u64().unwrap();
let drawn = res.state["drawn_points"].as_u64().unwrap();
assert_eq!(res.state["points_culled"], false, "a sparse layer is never a sea");
assert_eq!(drawn, vis, "sparse: drew {drawn} of {vis} visible");
}
}
/// **The colour.** A corner point is the building outline's own ink — the palette's
/// `building_stroke`, the SAME value `MapPalette::recolor_way` gives a building way
/// — and NOT the theme's `point` hue, which is what made them read blue on the
/// cartographic paper.
#[test]
fn visible_corners_are_the_building_stroke_colour() {
let v = OsmView::new("c", dense_town(100), vec![]);
let theme = facett_core::Theme::default();
for mode in [
facett_map::PaletteMode::CartographicDay,
facett_map::PaletteMode::CartographicNight,
facett_map::PaletteMode::Theme,
] {
let pal = mode.palette(&theme);
assert_eq!(
v.point_color(&pal, 0.0),
pal.building_stroke(),
"{}: a corner dot is the building OUTLINE ink",
mode.label()
);
// RED-when-broken against the old behaviour: it must NOT be the theme's
// point hue on the cartographic paper (that value is the reported blue).
if mode.is_cartographic() {
assert_ne!(v.point_color(&pal, 0.0), theme.point, "{}: not the theme point hue", mode.label());
}
}
}
/// **Debug mode** — default OFF; when ON it (a) bypasses the LOD cull so the
/// vertices are visible at the very zoom that normally hides them, and (b) paints
/// them in an unmistakable flashing colour that no cartographic palette contains.
#[test]
fn debug_mode_shows_the_culled_vertices_and_flashes() {
let r = rect();
let mut plain = OsmView::new("plain", dense_town(40_000), vec![]);
plain.fit_to(r);
assert!(!plain.debug_points(), "debug mode is OFF by default");
let res = harness::headless_render(&mut plain);
assert_eq!(res.state["drawn_points"], 0, "default: culled");
assert_eq!(res.state["debug_points"], false);
let mut dbg = OsmView::new("dbg", dense_town(40_000), vec![]).with_debug_points(true);
dbg.fit_to(r);
let res = harness::headless_render(&mut dbg);
assert!(res.state["drawn_points"].as_u64().unwrap() > 0, "debug shows what the LOD hid");
assert_eq!(res.state["points_culled"], false, "debug bypasses the cull");
assert_eq!(res.state["debug_points"], true);
// The flash: two distinct high-contrast phases, deterministic in time, and
// neither is a colour the cartographic palettes use.
let pal = facett_map::PaletteMode::CartographicDay.palette(&facett_core::Theme::default());
let a = dbg.point_color(&pal, 0.0);
let b = dbg.point_color(&pal, 0.25);
assert_ne!(a, b, "the debug ink FLASHES between two phases");
for c in [a, b] {
assert_ne!(c, pal.building_stroke(), "debug ink is never the map ink");
assert_ne!(c, pal.building, "debug ink is never the map ink");
assert_ne!(c, pal.land, "debug ink is never the map ink");
}
}
/// The debug view is reachable **from the UI**, not only from Rust: the pane's own
/// `Msg::SetDebugPoints` (which its header checkbox and the `V` key both emit) is a
/// real transition through the single update path, and it is observable in state.
#[test]
fn debug_mode_is_reachable_from_the_pane_and_observable() {
let mut v = OsmView::new("d", dense_town(40_000), vec![]);
let r = rect();
v.fit_to(r);
v.update(Msg::SetDebugPoints(true));
let on = harness::headless_render(&mut v);
assert_eq!(on.state["debug_points"], true);
assert!(on.state["drawn_points"].as_u64().unwrap() > 0, "toggling on reveals the vertices");
v.update(Msg::SetDebugPoints(false));
let off = harness::headless_render(&mut v);
assert_eq!(off.state["debug_points"], false);
assert_eq!(off.state["drawn_points"], 0, "toggling off restores the LOD cull");
}
/// The LOD is a **pure function of the ink**, so the painter's branch and a test's
/// prediction cannot drift apart: assert the boundary from both sides.
#[test]
fn the_ink_rule_is_exact_at_its_boundary() {
let r = rect();
let v = OsmView::new("i", vec![], vec![]);
let dot = OsmView::point_dot_px(1_000_000.0);
assert_eq!(dot, 3.0, "at high zoom the dot radius saturates at 3 px");
// Solve for the dot count that exactly spends the budget on this pane.
let pane = r.width() * r.height();
let per_dot = std::f32::consts::PI * dot * dot;
let budget_dots = (0.06 * pane / per_dot).floor() as usize;
assert!(v.points_pass_lod(budget_dots, dot, r), "{budget_dots} dots is within budget");
assert!(!v.points_pass_lod(budget_dots * 2, dot, r), "twice the budget is over");
// Zero dots is always fine (an empty layer must not be reported as a sea).
assert!(v.points_pass_lod(0, dot, r));
}