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

nordisk/facett

facett-geomap/tests/osm_palette.rs

//! **Robot proof (LAW #4) — the OSM 2D cartographic palette.**
//!
//! The `OsmView` basemap can paint with a fixed concrete-hex **cartographic**
//! palette (day = light paper / night = dark skade basemap) selected *alongside*
//! the theme-derived look via [`OsmView::cartographic`]. This test asserts three
//! things a beautify PR must not regress:
//!
//! 1. **state_json** advertises the selection (`palette == "cartographic-night"`),
//!    so a robot/host reads which look is live.
//! 2. **non-blank render** — the night preset renders through egui_kittest's wgpu
//!    backend and fills a visible region, and its land background is *dark* (the
//!    preset, not the theme, drove the paper colour).
//! 3. **alignment guard (<1px)** — selecting a palette is colour-only: it must
//!    NOT move a single projected vertex (the landed high-zoom correctness fix
//!    stays intact). Reskinning a way leaves its `points` / `lonlat` / `bbox`
//!    byte-identical.
//!
//! Run: `cargo test -p facett-geomap --test osm_palette`.

use egui_kittest::Harness;

use facett_core::Facet;
use facett_geomap::osm2d::{OsmView, sample_dataset};
use facett_map::{MapPalette, PaletteMode, Way};

/// Fraction of pixels differing from the modal (≈background) colour — a blank
/// pane scores ~0. (Same measure the basemap snapshot uses.)
fn content_ratio(img: &image::RgbaImage) -> f64 {
    let mut hist = std::collections::HashMap::<u32, u32>::new();
    for p in img.pixels() {
        let key = ((p.0[0] as u32 >> 3) << 10) | ((p.0[1] as u32 >> 3) << 5) | (p.0[2] as u32 >> 3);
        *hist.entry(key).or_default() += 1;
    }
    let (modal, _) = hist.iter().max_by_key(|(_, c)| **c).map(|(k, c)| (*k, *c)).unwrap();
    let (mr, mg, mb) = (((modal >> 10) & 0x1f) as i32, ((modal >> 5) & 0x1f) as i32, (modal & 0x1f) as i32);
    let mut content = 0usize;
    let total = (img.width() * img.height()) as usize;
    for p in img.pixels() {
        let (r, g, b) = ((p.0[0] >> 3) as i32, (p.0[1] >> 3) as i32, (p.0[2] >> 3) as i32);
        if (r - mr).abs() + (g - mg).abs() + (b - mb).abs() > 3 {
            content += 1;
        }
    }
    content as f64 / total as f64
}

/// The most common colour in the frame — the "paper" background.
fn modal_rgb(img: &image::RgbaImage) -> (u8, u8, u8) {
    let mut hist = std::collections::HashMap::<(u8, u8, u8), u32>::new();
    for p in img.pixels() {
        *hist.entry((p.0[0], p.0[1], p.0[2])).or_default() += 1;
    }
    hist.into_iter().max_by_key(|(_, c)| *c).map(|(k, _)| k).unwrap()
}

#[test]
fn osm_view_cartographic_palette_is_selectable_dark_and_non_blank() {
    // Default look = theme-derived.
    let default_view = OsmView::local();
    assert_eq!(default_view.state_json()["palette"], "theme", "default is the re-themeable look");

    // Select the night cartographic preset.
    let (points, ways) = sample_dataset(2_000);
    let mut view = OsmView::from_geometry("OSM 2D", points, ways).cartographic(false);
    let s = view.state_json();
    assert_eq!(s["palette"], "cartographic-night", "state_json advertises the selected preset");
    assert!(s["ways"].as_u64().unwrap() > 0, "there is a basemap to recolour");

    // Render headless (a light theme is set on the ctx — the preset must override
    // it and paint a DARK paper, proving the palette, not the theme, drove the bg).
    let mut harness = Harness::builder().with_size(egui::vec2(680.0, 680.0)).build_ui(move |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::default());
        view.ui(ui);
    });
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");
    let ratio = content_ratio(&img);
    eprintln!("[osm-cartographic] content ratio {ratio:.4}");
    assert!(ratio > 0.02, "the cartographic basemap must fill a visible region (got {ratio:.4})");

    // The night preset's land is #0B0F1A — a dark paper. Its channel sum is tiny;
    // a light theme background would be bright. This proves the preset overrode it.
    let (r, g, b) = modal_rgb(&img);
    let sum = r as u16 + g as u16 + b as u16;
    eprintln!("[osm-cartographic] paper rgb = ({r},{g},{b}) sum={sum}");
    assert!(sum < 220, "the night cartographic paper is dark (got sum {sum})");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit_non_blank("OsmView.cartographic_night", ratio, 0.02, s["ways"].as_u64().unwrap_or(0) as usize);
}

/// Count pixels within Manhattan distance `tol` (per-channel sum) of `target`.
fn count_near(img: &image::RgbaImage, target: (u8, u8, u8), tol: i32) -> usize {
    let (tr, tg, tb) = (target.0 as i32, target.1 as i32, target.2 as i32);
    img.pixels()
        .filter(|p| {
            (p.0[0] as i32 - tr).abs() + (p.0[1] as i32 - tg).abs() + (p.0[2] as i32 - tb).abs() <= tol
        })
        .count()
}

#[test]
fn cartographic_roads_get_a_dark_casing_under_the_bright_fill() {
    // The sample set has a motorway (the widest road → the strongest casing).
    let (points, ways) = sample_dataset(2_000);
    let mut view = OsmView::from_geometry("OSM 2D", points, ways).cartographic(true);
    let mut harness = Harness::builder().with_size(egui::vec2(640.0, 640.0)).build_ui(move |ui| {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::default());
        view.ui(ui);
    });
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");

    // Day casing is #B7AE9E — a mid grey-brown, distinct from both the light
    // paper (#F2EFE9) and the orange motorway fill (#E9A23B). If casing drew, a
    // band of these pixels rings the road.
    let casing = count_near(&img, (0xB7, 0xAE, 0x9E), 26);
    eprintln!("[casing] near-casing pixels = {casing}");
    assert!(casing > 30, "the cartographic road casing must paint a visible band (got {casing} px)");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-geomap::osm2d::OsmView::casing",
        "ui_render",
        casing > 30,
        &format!("casing_px={casing}"),
    );
}

#[test]
fn palette_select_is_colour_only_geometry_stays_within_1px() {
    // A filled water ring built in the theme look.
    let th = facett_core::Theme::default();
    let ring = [(9.50, 47.10), (9.53, 47.10), (9.53, 47.13), (9.50, 47.13), (9.50, 47.10)];
    let tags: Vec<(String, String)> =
        [("natural", "water")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
    let before = Way::from_lonlat(1, &ring, &tags, &th).unwrap();

    // Reskin to the cartographic day preset.
    let mut after = before.clone();
    MapPalette::cartographic(true).recolor_way(&mut after);

    // Colour changed …
    assert_ne!(after.stroke, before.stroke, "recolour actually reskinned the way");
    assert_ne!(after.fill, before.fill, "the fill hue switched to the preset");
    // … but not one projected vertex moved (the correctness fix is preserved).
    assert_eq!(after.points, before.points, "projected Mercator vertices are byte-identical");
    assert_eq!(after.lonlat, before.lonlat, "source lon/lat is untouched");
    assert_eq!(after.bbox, before.bbox, "the bbox is unchanged");

    // And PaletteMode resolves to the same concrete preset the view uses.
    assert_eq!(PaletteMode::CartographicDay.palette(&th).water, MapPalette::cartographic(true).water);
    assert!(PaletteMode::CartographicNight.is_cartographic());
    assert!(!PaletteMode::Theme.is_cartographic());
}