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());
}