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

nordisk/facett

facett-map3d/tests/garmin_overlays_robot.rs

//! **ROBOT-UI — Garmin nav overlays** (live egui_kittest render + data drive).
//!
//! Drives the LIVE `Map3D` navig Facet the way the web demo does (CPU painter) and
//! proves the three Garmin overlays on the real surface — both as PIXELS (they paint
//! into their expected screen regions) and as DATA (`state_json` exposes the bearing,
//! scale, and POI count):
//!   • NORTH ROSE — turning the overlays on paints the compass into the **top-left**;
//!     rotating the camera bearing changes `compass_bearing_deg`.
//!   • SCALE BAR — the scale bar paints into the **bottom-left** and `state_json`
//!     carries a metres value + a `"… m"/"… km"` label; dollying out grows it.
//!   • POI LAYER — turning the POI layer on paints markers (the frame changes) and
//!     `poi_count` reports how many draw; hiding a category drops the count.
//!   • ROADS — a route ribbon draws in the main 3-D view (the ground-plane fix), so a
//!     Garmin-style route is visible (guards the invisible-roads regression).
//!
//! Run: `cargo test -p facett-map3d --features testmatrix --test garmin_overlays_robot`

use egui_kittest::Harness;
use facett_core::{Facet, Theme};
use facett_map::{LayerCategory, Way};
use facett_map3d::{Map3D, Msg, PoiNode};

fn city() -> Vec<Way> {
    let mut ways = Vec::new();
    let mut id = 1;
    for r in 0..6 {
        for c in 0..6 {
            let bx = 17.70 + c as f64 * 0.0012;
            let by = 59.61 + r as f64 * 0.0012;
            let tags = vec![("building".to_string(), "yes".to_string())];
            let ring = [(bx, by), (bx + 0.0007, by), (bx + 0.0007, by + 0.0007), (bx, by + 0.0007), (bx, by)];
            ways.push(Way::from_lonlat(id, &ring, &tags, &Theme::default()).expect("a building way"));
            id += 1;
        }
    }
    ways
}

fn pois() -> Vec<PoiNode> {
    vec![
        PoiNode::new(17.7018, 59.6118, &[("amenity", "bar")], "Loki's"),
        PoiNode::new(17.7030, 59.6122, &[("shop", "kiosk")], ""),
        PoiNode::new(17.7024, 59.6130, &[("amenity", "restaurant")], "Grill"),
        PoiNode::new(17.7036, 59.6126, &[("amenity", "fuel")], "Q8"),
    ]
}

fn route_points() -> Vec<(f32, f32)> {
    let stops = vec![(17.7006, 59.6106), (17.7054, 59.6106), (17.7054, 59.6154)];
    Way::line(9001, "road", stops, egui::Color32::WHITE, 2.0).points
}

const SIZE: f32 = 480.0;

/// Render a map builder's output headless at a pinned clock; return the frame.
fn render(build: impl FnOnce(&[Way]) -> Map3D) -> image::RgbaImage {
    use std::cell::RefCell;
    use std::rc::Rc;
    let ways = city();
    let view = Rc::new(RefCell::new({
        let mut m = build(&ways);
        m.spin = false;
        m.set_clock(0.0);
        m
    }));
    let v2 = view.clone();
    let mut harness = Harness::builder().with_size(egui::vec2(SIZE, SIZE)).build_ui(move |ui| {
        Facet::ui(&mut *v2.borrow_mut(), ui);
    });
    harness.run_steps(3);
    harness.render().expect("headless render produced an image")
}

fn diff_in_region(a: &image::RgbaImage, b: &image::RgbaImage, x0: f32, y0: f32, x1: f32, y1: f32) -> u64 {
    let (w, h) = a.dimensions();
    let (px0, py0) = ((x0 * w as f32) as u32, (y0 * h as f32) as u32);
    let (px1, py1) = ((x1 * w as f32) as u32, (y1 * h as f32) as u32);
    let mut d = 0u64;
    for y in py0..py1.min(h) {
        for x in px0..px1.min(w) {
            if a.get_pixel(x, y) != b.get_pixel(x, y) {
                d += 1;
            }
        }
    }
    d
}

#[test]
fn garmin_overlays_paint_and_expose_data() {
    let ways = city();
    let pts = route_points();

    // ── PIXELS: overlays-off vs overlays-on differ in their expected regions ──────
    let plain = render(|w| Map3D::from_geometry("navig", w));
    let garmin = render(|w| {
        Map3D::from_geometry("navig", w)
            .with_route(w, &route_points())
            .with_pois(w, &pois())
            .show_pois(true)
            .with_compass(true)
            .with_scale_bar(true)
    });
    let compass_diff = diff_in_region(&plain, &garmin, 0.0, 0.0, 0.24, 0.24); // top-left rose
    let scale_diff = diff_in_region(&plain, &garmin, 0.0, 0.86, 0.4, 0.99); // bottom-left bar
    let scene_diff = diff_in_region(&plain, &garmin, 0.1, 0.2, 0.9, 0.8); // POI pins + road ribbon
    let compass_painted = compass_diff > 20;
    let scale_painted = scale_diff > 12;
    let scene_painted = scene_diff > 20;
    assert!(compass_painted, "the north rose paints into the top-left (diff={compass_diff} px)");
    assert!(scale_painted, "the scale bar paints into the bottom-left (diff={scale_diff} px)");
    assert!(
        scene_painted,
        "POI pins + the road route ribbon paint into the main 3-D view (diff={scene_diff}); \
         a 0 here is the invisible-roads / no-POI regression"
    );

    // ── DATA: state_json exposes bearing + scale + poi_count, and they respond ────
    use std::cell::RefCell;
    use std::rc::Rc;
    let view = Rc::new(RefCell::new(
        Map3D::from_geometry("navig", &ways)
            .with_route(&ways, &pts)
            .with_pois(&ways, &pois())
            .show_pois(true)
            .with_compass(true)
            .with_scale_bar(true),
    ));
    let v2 = view.clone();
    let mut harness = Harness::builder().with_size(egui::vec2(SIZE, SIZE)).build_ui(move |ui| {
        Facet::ui(&mut *v2.borrow_mut(), ui);
    });
    harness.run_steps(2);

    let j0 = view.borrow().state_json();
    let bearing0 = j0["compass_bearing_deg"].as_f64().unwrap();
    let scale_m0 = j0["scale_metres"].as_f64().unwrap();
    let scale_label = j0["scale_label"].as_str().unwrap().to_string();
    let poi_total = j0["poi_total"].as_u64().unwrap();
    let poi_on = j0["poi_count"].as_u64().unwrap();
    assert_eq!(poi_total, 4, "all 4 POIs attached");
    assert_eq!(poi_on, 4, "the POI layer reports all 4 drawing");
    assert!(scale_m0 > 0.0, "the scale bar reports a positive ground distance");
    assert!(
        scale_label.ends_with('m') || scale_label.ends_with("km"),
        "the scale label carries a unit ({scale_label:?})"
    );
    assert!(j0["compass"].as_bool().unwrap() && j0["scale_bar"].as_bool().unwrap() && j0["show_pois"].as_bool().unwrap());

    // Rotate the bearing → compass_bearing_deg tracks it.
    view.borrow_mut().update(Msg::SetCameraPose {
        target: None,
        azimuth: Some(bearing0 as f32 + 1.2),
        elevation: None,
        distance: None,
        fov_y: None,
    });
    harness.run_steps(2);
    let bearing1 = view.borrow().state_json()["compass_bearing_deg"].as_f64().unwrap();
    let bearing_moved = (bearing1 - bearing0).abs() > 1.0;
    assert!(bearing_moved, "rotating the camera moves the compass bearing ({bearing0:.1}° → {bearing1:.1}°)");

    // Dolly OUT → the scale distance grows.
    let far_distance = view.borrow().render_camera().distance * 6.0;
    view.borrow_mut().update(Msg::SetCameraPose {
        target: None,
        azimuth: None,
        elevation: None,
        distance: Some(far_distance),
        fov_y: None,
    });
    harness.run_steps(2);
    let scale_m1 = view.borrow().state_json()["scale_metres"].as_f64().unwrap();
    let scale_grew = scale_m1 > scale_m0;
    assert!(scale_grew, "dollying out grows the scale distance ({scale_m0} → {scale_m1} m)");

    // Hide the Bars category → poi_count drops.
    view.borrow_mut().set_poi_layer(LayerCategory::Bars, false);
    harness.run_steps(1);
    let poi_hidden = view.borrow().state_json()["poi_count"].as_u64().unwrap();
    let poi_dropped = poi_hidden < poi_on;
    assert!(poi_dropped, "hiding a POI category drops the count ({poi_on} → {poi_hidden})");

    #[cfg(feature = "testmatrix")]
    {
        let ok = compass_painted
            && scale_painted
            && scene_painted
            && poi_total == 4
            && poi_on == 4
            && bearing_moved
            && scale_grew
            && poi_dropped;
        facett_core::testmatrix::emit(
            "facett-map3d",
            "garmin_overlays_paint_and_expose_data",
            ok,
            &format!(
                "compass_diff={compass_diff} scale_diff={scale_diff} scene_diff={scene_diff} \
                 poi_total={poi_total} poi_on={poi_on} poi_hidden={poi_hidden} \
                 bearing {bearing0:.1}->{bearing1:.1} scale {scale_m0}->{scale_m1} label={scale_label:?}"
            ),
        );
    }
}