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

nordisk/facett

facett-map/tests/callchain_matrix.rs

//! **Render call-chain coverage matrix** for `facett-map` (LAW: every fn in the
//! input→pixel chain TRACKED + VERIFIED, return-asserted where it proves
//! correctness, emit where it can't).
//!
//! The 2D map chain is:
//!
//! ```text
//!   read ways/points (lon,lat)
//!     → project Mercator      (mercator / project::to_mercator_f32 / Way::project)
//!     → classify/style        (style::classify → WayStyle{is_area,fill,lod,kind})
//!     → ring close test       (Way::from_lonlat → fill iff is_area && closed)
//!     → tessellate areas      (tessellate::triangulate / filled_mesh ear-clip)
//!     → LOD partition + cull  (index::MapIndex::build / query_visible / gpu_ways)
//!     → fit-to-data bbox      (MapView::merc_bbox via bbox + Way.bbox)
//!     → paint lines+fills+pts (Facet::ui CPU painter)
//!     → pixels                (egui_kittest wgpu render — robot/headless)
//! ```
//!
//! Each stage gets an **inject real input → assert the real return** case; the
//! painted-pixel stage (where the return doesn't expose the rasterized output) is
//! covered by the EMIT-doctrine non-blank / fills-not-just-lines / confined
//! checks (the `testmatrix`-gated `emit*` rows + the egui_kittest robot drive).
//!
//! ~1000 matrix cases: geometry kinds {road/water/landuse/building/coastline} ×
//! zoom levels × extents × LOD tiers × canvas sizes, fed real WKB/GeoParquet-
//! shaped `(lon,lat)` input.
//!
//! Run: `cargo test -p facett-map --test callchain_matrix`
//! Matrix rows: `cargo test -p facett-map --features testmatrix --test callchain_matrix`

use facett_core::{Theme, harness};
use facett_map::style::classify;
use facett_map::tessellate::{filled_mesh, triangulate};
use facett_map::{MapView, Way, WayKind, index::MapIndex, mercator, project};

// ── matrix axes ───────────────────────────────────────────────────────────────

/// The five geometry kinds the LAW names, each as a (tag-pair, is-area-expected,
/// expected WayKind) triple — the classify oracle.
const KINDS: &[(&str, &str, bool, WayKind)] = &[
    ("highway", "primary", false, WayKind::Road),
    ("natural", "water", true, WayKind::Water),
    ("landuse", "forest", true, WayKind::Park),
    ("building", "yes", true, WayKind::Building),
    ("natural", "coastline", false, WayKind::Coastline),
];

/// A spread of lon/lat extents (degrees) — country / region / city / street.
const EXTENTS: &[f64] = &[8.0, 2.0, 0.5, 0.1, 0.02];

/// Canvas sizes the host might hand the pane (incl. a squeezed one).
const CANVASES: &[(f32, f32)] = &[(160.0, 120.0), (400.0, 300.0), (800.0, 600.0), (1280.0, 720.0), (240.0, 900.0)];

/// Base centres across the globe (lon, lat) — varied Mercator y (incl. both
/// hemispheres + high-latitude). 8 centres so the full sweep clears 1000 cells.
const CENTRES: &[(f64, f64)] = &[
    (0.0, 0.0),
    (9.55, 47.15),
    (18.07, 59.33),
    (-122.4, 37.8),
    (139.7, 35.7),
    (151.2, -33.9),
    (-43.2, -22.9),
    (37.6, 55.75),
];

/// Build a closed square ring of `ext` degrees around `(lon,lat)`.
fn ring(lon: f64, lat: f64, ext: f64) -> Vec<(f64, f64)> {
    let h = ext / 2.0;
    vec![(lon - h, lat - h), (lon + h, lat - h), (lon + h, lat + h), (lon - h, lat + h), (lon - h, lat - h)]
}

/// Build an open 2-vertex line of `ext` degrees from `(lon,lat)`.
fn line(lon: f64, lat: f64, ext: f64) -> Vec<(f64, f64)> {
    vec![(lon, lat), (lon + ext, lat + ext * 0.5)]
}

fn tagvec(k: &str, v: &str) -> Vec<(String, String)> {
    vec![(k.to_string(), v.to_string())]
}

// ── stage 1: projection (mercator / to_mercator_f32 / Way::project) ────────────

/// INJECT-ASSERT — the projection's RETURN proves correctness: x is monotone in
/// lon, y is monotone (inverted) in lat, centre→(0.5,0.5), and the f32 GPU
/// projection matches the f64 viewer projection. Walked over every centre/extent.
#[test]
fn projection_returns_correct_monotone_mercator() {
    let mut cases = 0usize;
    let mut ok = 0usize;
    for &(lon, lat) in CENTRES {
        // centre identity at the origin
        let (cx, cy) = mercator(0.0, 0.0);
        assert!((cx - 0.5).abs() < 1e-9 && (cy - 0.5).abs() < 1e-9);
        for &ext in EXTENTS {
            // east is +x
            let (xw, _) = mercator(lon, lat);
            let (xe, _) = mercator((lon + ext).min(179.0), lat);
            assert!(xe > xw, "lon+{ext} → larger x at lon {lon}");
            // north is smaller y (up)
            let (_, ys) = mercator(lon, (lat - ext).max(-84.0));
            let (_, yn) = mercator(lon, (lat + ext).min(84.0));
            assert!(yn < ys, "north → smaller y at lat {lat}");
            // f32 GPU projection matches f64 viewer projection (same shape)
            let (fx, fy) = project::to_mercator_f32(lon as f32, lat as f32);
            let (dx, dy) = mercator(lon, lat);
            assert!((fx as f64 - dx).abs() < 1e-4, "f32 x matches f64");
            assert!((fy as f64 - dy).abs() < 1e-4, "f32 y matches f64");
            // Way::project (via from_lonlat) computes a bbox that contains the verts
            let w = Way::from_lonlat(1, &ring(lon, lat, ext), &tagvec("natural", "water"), &Theme::default()).unwrap();
            // The bbox is f64 and derived from the f64 Mercator of `lonlat`
            // (stinky `seam-15`); `points` is the deliberately coarse f32 stream. So
            // the containment holds to the f32 stream's OWN tolerance, not to the
            // bbox's — which is the honest statement of what these two are.
            for &(px, py) in &w.points {
                assert!(f64::from(px) >= w.bbox[0] - 1e-6 && f64::from(px) <= w.bbox[2] + 1e-6, "vertex x in bbox");
                assert!(f64::from(py) >= w.bbox[1] - 1e-6 && f64::from(py) <= w.bbox[3] + 1e-6, "vertex y in bbox");
            }
            cases += 1;
            ok += 1;
        }
    }
    assert_eq!(cases, CENTRES.len() * EXTENTS.len());
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.project",
        "mercator_monotone_and_f32_matches_f64",
        ok == cases,
        &format!("{ok}/{cases} projection cases"),
    );
    assert_eq!(ok, cases, "every case in the sweep passed ({ok}/{cases})");
}

// ── stage 2+3: classify → style → ring-close → fill (the WKB-shaped reader) ────

/// INJECT-ASSERT — `classify`'s RETURN (`WayStyle`) carries the correct kind +
/// is_area + fill, and `Way::from_lonlat` only fills a CLOSED area ring (an open
/// area still strokes). Walked over every kind × extent × centre = the classify
/// matrix.
#[test]
fn classify_and_ring_close_decide_fill_correctly() {
    let th = Theme::default();
    let mut cases = 0usize;
    let mut ok = 0usize;
    for &(k, v, want_area, want_kind) in KINDS {
        // pure classify return
        let style = classify(&tagvec(k, v), &th);
        assert_eq!(style.is_area, want_area, "{k}={v} is_area");
        assert_eq!(style.kind, want_kind, "{k}={v} kind");
        assert_eq!(style.fill.is_some(), want_area, "fill set iff area for {k}={v}");
        for &(lon, lat) in CENTRES {
            for &ext in EXTENTS {
                // CLOSED ring: an area fills, a line never does.
                let closed = Way::from_lonlat(1, &ring(lon, lat, ext), &tagvec(k, v), &th).unwrap();
                assert_eq!(closed.fill.is_some(), want_area, "closed {k}={v} fill matches area-ness");
                assert_eq!(closed.style.kind, want_kind);
                // OPEN way of the SAME tags: an area must NOT fill (it strokes
                // gracefully); a line stays a line.
                let open = Way::from_lonlat(1, &line(lon, lat, ext), &tagvec(k, v), &th).unwrap();
                assert!(open.fill.is_none(), "an OPEN {k}={v} way never fills (graceful)");
                cases += 2;
                ok += 2;
            }
        }
    }
    // road width ordering (the style return drives the painter's stroke width)
    let mw = classify(&tagvec("highway", "motorway"), &th).width;
    let rs = classify(&tagvec("highway", "residential"), &th).width;
    assert!(mw > rs, "motorway wider than residential");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.classify",
        "kind_area_fill_and_ring_close",
        ok == cases,
        &format!("{ok}/{cases} classify/ring cases over {} kinds", KINDS.len()),
    );
    assert_eq!(ok, cases, "every case in the sweep passed ({ok}/{cases})");
}

// ── stage 4: tessellation (ear-clip) — area equals the true shoelace area ──────

/// INJECT-ASSERT — the ear-clip's RETURN is provably correct: the summed triangle
/// area equals the polygon's true shoelace area (a convex fan would over-count a
/// concave ring), and a simple ring yields exactly n-2 triangles. Walked over a
/// family of synthetic rings (convex squares + concave notches) at varied scales.
#[test]
fn tessellation_area_equals_true_area_concave_safe() {
    use egui::Pos2;
    let scales = [1.0_f32, 10.0, 100.0, 1000.0];
    // Build rings at several scales: a convex quad and a concave "arrow".
    for &scale in &scales {
        let quad =
            [Pos2::new(0.0, 0.0), Pos2::new(scale, 0.0), Pos2::new(scale, scale), Pos2::new(0.0, scale)];
        let tq = triangulate(&quad);
        assert_eq!(tq.len(), 2, "quad → 2 tris at scale {scale}");
        let area_q: f32 = tq
            .iter()
            .map(|&[a, b, c]| tri_area(quad[a as usize], quad[b as usize], quad[c as usize]))
            .sum();
        assert!((area_q - scale * scale).abs() < scale * scale * 1e-3, "quad area at {scale}");

        // concave pentagon with an inward notch
        let poly = [
            Pos2::new(0.0, 0.0),
            Pos2::new(2.0 * scale, 0.0),
            Pos2::new(2.0 * scale, 2.0 * scale),
            Pos2::new(scale, 0.5 * scale), // the notch
            Pos2::new(0.0, 2.0 * scale),
        ];
        let tp = triangulate(&poly);
        assert_eq!(tp.len(), poly.len() - 2, "n-2 tris for a simple polygon at {scale}");
        let true_area = shoelace(&poly);
        let tp_area: f32 = tp
            .iter()
            .map(|&[a, b, c]| tri_area(poly[a as usize], poly[b as usize], poly[c as usize]))
            .sum();
        assert!(
            (tp_area - true_area).abs() < true_area * 1e-2,
            "concave area {tp_area} == true {true_area} at {scale} (no spill into the notch)"
        );
        // filled_mesh produces a real mesh for a valid ring, None for a degenerate one
        assert!(filled_mesh(&quad, egui::Color32::RED).is_some(), "valid ring → mesh");
        assert!(filled_mesh(&quad[..2], egui::Color32::RED).is_none(), "2-vert ring → None");
    }
    // All scales above passed their asserts (else we'd have panicked).
    let cases = scales.len();
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.tessellate",
        "earclip_area_equals_shoelace",
        true,
        &format!("{cases} ear-clip scales (concave-safe)"),
    );
    let _ = cases;
}

fn tri_area(a: egui::Pos2, b: egui::Pos2, c: egui::Pos2) -> f32 {
    ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() / 2.0
}
fn shoelace(p: &[egui::Pos2]) -> f32 {
    let mut a = 0.0;
    for i in 0..p.len() {
        let q = p[(i + 1) % p.len()];
        a += p[i].x * q.y - q.x * p[i].y;
    }
    a.abs() / 2.0
}

// ── stage 5: LOD partition + viewport cull (index::MapIndex) ───────────────────

/// INJECT-ASSERT — `MapIndex` partitions ways into the right LOD tier, `gpu_ways`
/// walks every tier (count conserved), and `query_visible`'s RETURN is monotone:
/// a higher LOD never returns FEWER ways, and a tighter viewport never returns
/// MORE. Walked over a synthetic basemap built across kinds × extents.
#[test]
fn lod_partition_and_cull_counts_are_monotone() {
    let th = Theme::default();
    let mut ways = Vec::new();
    for &(k, v, _, _) in KINDS {
        for (i, &(lon, lat)) in CENTRES.iter().enumerate() {
            for (j, &ext) in EXTENTS.iter().enumerate() {
                let geom = if classify(&tagvec(k, v), &th).is_area { ring(lon, lat, ext) } else { line(lon, lat, ext) };
                if let Some(w) = Way::from_lonlat((i * 100 + j) as i64, &geom, &tagvec(k, v), &th) {
                    ways.push(w);
                }
            }
        }
    }
    let total = ways.len();
    assert!(total > 50, "built a real basemap of {total} ways");
    let idx = MapIndex::build(ways);
    let sizes = idx.lod_sizes();
    assert_eq!(
        sizes.iter().sum::<usize>(),
        total,
        "every way landed in exactly one of the {} tiers ({sizes:?})",
        sizes.len()
    );
    assert_eq!(idx.gpu_ways().count(), total, "gpu_ways walks every tier");
    assert_eq!(idx.len(), total);

    // LOD monotonicity: visible(lod=0) ⊆ visible(lod=1) ⊆ visible(lod=2).
    let vp = [0.0_f32, 0.0, 1.0, 1.0];
    // Walk EVERY tier, not the three that existed when this was written. The tier
    // count is `lod_sizes().len()`, so a fourth tier is covered here by construction —
    // the old form stopped at `lod=2` and would have reported a clean monotone pass
    // over a map whose deepest tier it never asked about.
    let counts: Vec<usize> =
        (0..sizes.len() as u32).map(|l| idx.query_visible(vp, l).len()).collect();
    assert!(
        counts.windows(2).all(|w| w[0] <= w[1]),
        "higher LOD never shows fewer ways: {counts:?}"
    );
    assert_eq!(
        *counts.last().unwrap(),
        total,
        "the whole-world viewport at the DEEPEST tier sees every way: {counts:?}"
    );
    // Viewport monotonicity: a tighter box never returns more than a wider box.
    let wide = idx.query_visible([0.0, 0.0, 1.0, 1.0], 2).len();
    let tight = idx.query_visible([0.5, 0.5, 0.55, 0.55], 2).len();
    assert!(tight <= wide, "tighter viewport returns no more ways: {tight} <= {wide}");
    // An empty viewport off the data returns nothing.
    let off = idx.query_visible([0.99, 0.99, 1.0, 1.0], 2).len();
    assert!(off <= wide);

    // The row restates the SAME quantities the asserts above walked — `sizes` and
    // `counts`, both of which follow `lod_sizes().len()`. The form that stood here
    // named `c`, `r`, `t`, `n0`, `n1` — five bindings that stopped existing when the
    // three hardcoded tiers became the `counts` walk, so this file has not COMPILED
    // under `--features testmatrix` since. A row that cannot be built emits nothing,
    // and nothing is indistinguishable from a row nobody wrote.
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.index",
        "lod_partition_and_cull_monotone",
        sizes.iter().sum::<usize>() == total
            && counts.windows(2).all(|w| w[0] <= w[1])
            && *counts.last().unwrap() == total
            && tight <= wide,
        &format!(
            "tiers={sizes:?} total={total} visible_per_lod={counts:?} tight={tight}<=wide={wide}"
        ),
    );
}

// ── stage 6: fit-to-data bbox (MapView::merc_bbox via bbox) ────────────────────

/// INJECT-ASSERT — the fit-to-data bbox (`MapView::bbox`) returns a box that
/// CONTAINS every injected lon/lat across both layers, growing to cover a basemap
/// added on top. Walked over centre/extent combos.
#[test]
fn fit_to_data_bbox_contains_all_features() {
    let th = Theme::default();
    let mut cases = 0usize;
    let mut ok = 0usize;
    for &(lon, lat) in CENTRES {
        for &ext in EXTENTS {
            let pts = vec![(lon, lat), (lon + ext, lat + ext)];
            let ways = vec![Way::from_lonlat(1, &ring(lon, lat, ext), &tagvec("natural", "water"), &th).unwrap()];
            let v = MapView::new("m", pts.clone()).with_ways(ways);
            let (mnlon, mnlat, mxlon, mxlat) = v.bbox().expect("non-empty map has a bbox");
            // contains every point
            for &(px, py) in &pts {
                assert!(px >= mnlon - 1e-9 && px <= mxlon + 1e-9, "lon {px} in bbox");
                assert!(py >= mnlat - 1e-9 && py <= mxlat + 1e-9, "lat {py} in bbox");
            }
            // contains the way ring (the ring reaches lon-ext/2 west of centre)
            assert!(mnlon <= lon - ext / 2.0 + 1e-9, "bbox grew to cover the way west edge");
            cases += 1;
            ok += 1;
        }
    }
    // an empty map has no bbox (graceful)
    assert!(MapView::new("e", vec![]).bbox().is_none());
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.fit",
        "merc_bbox_contains_all_features",
        ok == cases,
        &format!("{ok}/{cases} fit-to-data cases"),
    );
    assert_eq!(ok, cases, "every case in the sweep passed ({ok}/{cases})");
}

// ── stage 7: paint (headless CPU painter) — the ~1000-case render sweep ────────

/// **THE MATRIX** (kinds × extents × centres × canvases). For each cell: build a
/// real WKB/GeoParquet-shaped MapView (a closed area ring of the kind + a line
/// way + a few points), render it headless at the canvas size, and:
///  - inject-ASSERT the state_json (ways/points/filled counts) is exactly right,
///  - EMIT (return can't expose the raster): the headless render DREW vertices
///    (non-blank), with non-zero geometry.
///
/// This is the input→pixel proof for the CPU painter across the whole axis grid.
#[test]
fn render_matrix_kinds_x_extents_x_centres_x_canvases() {
    let th = Theme::default();
    let mut cases = 0usize;
    let mut drew = 0usize;
    let mut filled_correct = 0usize;
    for &(k, v, want_area, _) in KINDS {
        for &(lon, lat) in CENTRES {
            for &ext in EXTENTS {
                // a closed ring of this kind + a road line + 3 points
                let area = Way::from_lonlat(1, &ring(lon, lat, ext), &tagvec(k, v), &th).unwrap();
                let road = Way::from_lonlat(2, &line(lon, lat, ext), &tagvec("highway", "primary"), &th).unwrap();
                let pts = vec![(lon, lat), (lon + ext * 0.3, lat), (lon, lat + ext * 0.3)];
                let want_filled = if want_area { 1 } else { 0 };
                for &(cw, ch) in CANVASES {
                    let mut view = MapView::new("cell", pts.clone()).with_ways(vec![area.clone(), road.clone()]);
                    let r = harness::render_sized(&mut view, (cw, ch));
                    // inject-assert state
                    assert_eq!(r.state["points"], 3, "3 points {k}={v} {cw}x{ch}");
                    assert_eq!(r.state["ways"], 2, "area + road");
                    assert_eq!(
                        r.state["filled_ways"].as_u64().unwrap() as i64,
                        want_filled,
                        "{k}={v} filled_ways == {want_filled}"
                    );
                    // EMIT-doctrine: the painter actually drew (non-blank proxy).
                    if r.drew() {
                        drew += 1;
                    }
                    if (r.state["filled_ways"].as_u64().unwrap() as i64) == want_filled {
                        filled_correct += 1;
                    }
                    cases += 1;
                }
            }
        }
    }
    assert!(cases >= 1000, "the matrix has >= 1000 cases, got {cases}");
    assert_eq!(drew, cases, "every populated cell drew vertices (non-blank), {drew}/{cases}");
    assert_eq!(filled_correct, cases, "every cell's filled-vs-line classification held");
    eprintln!("[map-matrix] {cases} cells, all drew, all filled-classified correctly");
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.render_matrix",
        "all_cells_draw_with_correct_fill",
        drew == cases && filled_correct == cases,
        &format!("{cases} cells (kinds×extents×centres×canvases), {drew} drew, {filled_correct} fill-correct"),
    );
}

/// Empty-map negative cell: a populated cell draws, an EMPTY map is (correctly)
/// near-blank — proving the non-blank proxy is not vacuous on the headless path.
#[test]
fn empty_cell_is_distinguishable_from_a_populated_one() {
    let mut empty = MapView::new("empty", vec![]);
    let re = harness::headless_render(&mut empty);
    assert_eq!(re.state["points"], 0);
    assert_eq!(re.state["ways"], 0);
    // The hint text draws a little, but a populated map draws far more vertices.
    let mut full = MapView::new("full", (0..200).map(|i| (9.5 + i as f64 * 0.001, 47.1)).collect());
    let rf = harness::headless_render(&mut full);
    assert!(rf.vertices > re.vertices * 2, "populated >> empty: {} vs {}", rf.vertices, re.vertices);
    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map.render_matrix",
        "empty_vs_populated_separable",
        rf.vertices > re.vertices * 2,
        &format!("empty={} populated={} verts", re.vertices, rf.vertices),
    );
}