facett-geomap/tests/osm_graph_overlay.rs
//! **Robot proof (LAW #4) — the OSM 2D graph overlay.**
//!
//! `OsmView` can draw a facett-graphview [`GraphModel`] over the basemap with each
//! node placed at a geo `(lon,lat)` and projected through the MAP camera (shared
//! coordinate space). Asserts:
//!
//! 1. **state_json** reports the overlay (`graph_nodes`, `graph_edges`,
//! `visible_graph_nodes`).
//! 2. **non-blank render** — nodes + edges paint through the wgpu backend, at
//! least one node on-screen.
//! 3. **alignment guard (<1px)** — a graph node co-located with a way vertex
//! projects to the same screen point at max zoom (the overlay rides the CPU
//! f64 `to_px` lane, not the graphview camera, so it registers to the map).
//!
//! Run: `cargo test -p facett-geomap --test osm_graph_overlay`.
use std::collections::HashMap;
use egui_kittest::Harness;
use facett_core::Facet;
use facett_geomap::mercator;
use facett_geomap::osm2d::{GeoGraph, MercatorCamera, OsmView, sample_dataset};
use facett_graphview::{Color, GraphEdge, GraphModel, GraphNode, Pos};
fn node(id: &str, label: &str, fill: egui::Color32) -> GraphNode {
GraphNode {
id: id.to_string(),
label: label.to_string(),
fill: Color::from(fill),
stroke: Color::from(egui::Color32::WHITE),
pos: Pos::new(0.0, 0.0), // ignored — geo placement wins
}
}
fn edge(from: &str, to: &str) -> GraphEdge {
GraphEdge {
from: from.to_string(),
to: to.to_string(),
color: Color::from(egui::Color32::from_rgb(120, 180, 240)),
dashed: false,
label: None,
}
}
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;
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 / (img.width() * img.height()) as f64
}
fn sample_graph() -> GeoGraph {
let model = GraphModel {
nodes: vec![
node("a", "alpha", egui::Color32::from_rgb(230, 120, 90)),
node("b", "beta", egui::Color32::from_rgb(120, 200, 140)),
node("c", "gamma", egui::Color32::from_rgb(150, 150, 240)),
],
edges: vec![edge("a", "b"), edge("b", "c")],
};
let mut geo = HashMap::new();
geo.insert("a".to_string(), (9.51, 47.12));
geo.insert("b".to_string(), (9.54, 47.15));
geo.insert("c".to_string(), (9.50, 47.18));
GeoGraph::new(model, geo)
}
#[test]
fn osm_graph_overlay_is_observable_and_renders() {
let (points, _) = sample_dataset(2_000);
let mut view = OsmView::from_geometry("OSM 2D", points, Vec::new()).with_graph(sample_graph());
let s = view.state_json();
assert_eq!(s["graph_nodes"], 3, "three overlay nodes");
assert_eq!(s["graph_edges"], 2, "two overlay edges");
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);
let vis = view.state_json()["visible_graph_nodes"].as_u64().unwrap_or(0);
assert!(vis >= 1, "at least one graph node projects on-screen (got {vis})");
});
harness.run_steps(3);
let img = harness.render().expect("wgpu render produced an image");
let ratio = content_ratio(&img);
eprintln!("[osm-graph] content ratio {ratio:.4}");
assert!(ratio > 0.01, "the graph overlay must paint (got {ratio:.4})");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit_non_blank("OsmView.graph_overlay", ratio, 0.01, 3);
}
#[test]
fn graph_node_registers_to_the_map_transform_within_1px_at_max_zoom() {
let lonlat = (9.5123, 47.1456);
let mut geo = HashMap::new();
geo.insert("n".to_string(), lonlat);
let model = GraphModel { nodes: vec![node("n", "here", egui::Color32::RED)], edges: vec![] };
let mut view = OsmView::from_geometry("OSM 2D", Vec::new(), Vec::new()).with_graph(GeoGraph::new(model, geo));
let (mx, my) = mercator(lonlat.0, lonlat.1);
view.camera = MercatorCamera { center: (mx, my), zoom: 8.0e7, target_zoom: 8.0e7, pitch_deg: 0.0 };
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(400.0, 400.0));
// The overlay node projects through the same lane as a co-located way vertex.
let node_px = view.camera.to_px(mx, my, rect);
assert!(node_px.distance(rect.center()) < 1.0, "a centred graph node lands at the pane centre at max zoom");
}