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/route_proof.rs

//! **OSM-3D route overlay + turn-by-turn follow proof** (headless, data-level A/B —
//! no pixels, no GPU).
//!
//! The car-navigator **route overlay** projects a waypoint chain onto the extruded
//! streets, bakes a bright "your route" ribbon + waypoint markers into the basemap
//! mesh, and (follow mode) flies the camera along it. This asserts, as DATA:
//!   1. attaching a route **adds geometry** (triangles + a tile) to the basemap and
//!      surfaces `has_route` / `route_waypoints` / `route_length` in `state_json`,
//!   2. seeking progress `t` while following **moves the camera target** along the
//!      route (monotone advance from start to destination),
//!   3. the route **bearing turns** across the L-corner (turn-by-turn direction),
//!   4. the follow pose is **deterministic** (a pure function of `t`), and
//!   5. follow **off** leaves the camera un-driven by the route.
//!
//! Run: `cargo test -p facett-map3d --test route_proof [--features testmatrix]`.

use facett_core::{Facet, Theme};
use facett_map::Way;
use facett_map3d::Map3D;

/// A tiny 6×6 building grid around Uppsala-ish (17.70E, 59.61N) — the streets the
/// route drapes over.
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
}

/// An **L-shaped** route (lon/lat) crossing the grid: east then north. Returned as
/// Mercator `Way::points` (the space `Map3D::with_route` expects) by building a
/// line `Way` and reading its projected points — so the route lands ON the streets.
fn l_route_points() -> Vec<(f32, f32)> {
    let stops = vec![(17.7006, 59.6106), (17.7054, 59.6106), (17.7054, 59.6154)];
    let w = Way::line(9001, "road", stops, egui::Color32::WHITE, 2.0);
    w.points
}

/// The same 6×6 grid and the same L route, but spread over `span_deg` degrees — so the
/// SAME topology can be asked at two scene scales and a constant that failed to track
/// the scale shows up as a ratio instead of hiding behind one plausible number.
fn city_and_route_spanning(span_deg: f64) -> (Vec<Way>, Vec<(f32, f32)>) {
    let (lon0, lat0) = (17.70_f64, 59.61_f64);
    let step = span_deg / 6.0;
    let mut ways = Vec::new();
    let mut id = 1;
    for r in 0..6 {
        for c in 0..6 {
            let (bx, by) = (lon0 + c as f64 * step, lat0 + r as f64 * step);
            let d = step * 0.58;
            let tags = vec![("building".to_string(), "yes".to_string())];
            let ring = [(bx, by), (bx + d, by), (bx + d, by + d), (bx, by + d), (bx, by)];
            ways.push(Way::from_lonlat(id, &ring, &tags, &Theme::default()).expect("a building way"));
            id += 1;
        }
    }
    let stops = vec![
        (lon0 + step * 0.5, lat0 + step * 0.5),
        (lon0 + step * 4.5, lat0 + step * 0.5),
        (lon0 + step * 4.5, lat0 + step * 4.5),
    ];
    let w = Way::line(9001, "road", stops, egui::Color32::WHITE, 2.0);
    (ways, w.points)
}

/// **The turn-by-turn follow camera sits a real street distance behind the car — at
/// every scene scale.**
///
/// `Map3D::ROUTE_CHASE_DIST` was `OrbitCamera::CHASE_DIST` verbatim: `0.55` DESIGN
/// UNITS, documented as "close in, like a bonnet/hood cam". A design unit is whatever
/// the data's span makes it, so that bonnet cam sat ~950 m back in `korp`'s 3 km town
/// window and ~16 km back on a region clip. `tour.rs` had already walked around the
/// same constant with a metric `STREET_CHASE_M`; `apply_route_camera` had not, so the
/// crate held two different opinions about where a chase camera goes.
///
/// Asserted at two spans a decade apart, on the APPLIED pose (`state_json`'s camera
/// distance), not on the constant.
#[test]
fn the_follow_chase_is_a_real_street_distance_at_every_scene_scale() {
    for &span_deg in &[0.0270_f64, 0.4500] {
        let (ways, route) = city_and_route_spanning(span_deg);
        let mut m = Map3D::from_ways("OSM 3D", &ways, 0.2)
            .with_route(&ways, &route)
            .with_route_follow(true);
        m.set_clock(0.0);
        m.update(facett_map3d::Msg::RouteProgress(0.4));
        let mpu = m.metres_per_design_unit();
        let dist_du = m.state_json()["camera"]["distance"].as_f64().expect("camera.distance") as f32;
        let dist_m = dist_du * mpu;
        println!(
            "  span {span_deg:.4}°  {mpu:>10.1} m/du   chase {dist_du:.4} du = {dist_m:>9.1} m \
             (want {} m)",
            facett_map3d::tour::STREET_CHASE_M
        );
        assert!(
            (dist_m - facett_map3d::tour::STREET_CHASE_M).abs() < 2.0,
            "the follow camera sits {dist_m:.0} m behind the car at {mpu:.0} m/du — a bonnet \
             cam is {} m, whatever the scene's span",
            facett_map3d::tour::STREET_CHASE_M
        );
    }
}

/// Read `state_json`'s camera target `[x, y, z]`.
fn cam_target(m: &Map3D) -> [f32; 3] {
    let j = m.state_json();
    let t = j["camera"]["target"].as_array().expect("camera.target");
    [t[0].as_f64().unwrap() as f32, t[1].as_f64().unwrap() as f32, t[2].as_f64().unwrap() as f32]
}

#[test]
fn route_renders_and_the_follow_camera_chases_it() {
    let ways = city();
    let route = l_route_points();

    // (1) Attaching a route ADDS geometry + surfaces route data.
    let plain = Map3D::from_ways("OSM 3D", &ways, 0.2);
    let routed = Map3D::from_ways("OSM 3D", &ways, 0.2).with_route(&ways, &route);
    let tris_added = routed.triangle_count() as i64 - plain.triangle_count() as i64;
    let added_geom = tris_added > 0 && routed.tile_count() > plain.tile_count();
    assert!(added_geom, "the route ribbon + markers add triangles ({tris_added}) and a tile");

    let js = routed.state_json();
    let has_route = js["has_route"].as_bool().unwrap();
    let waypoints = js["route_waypoints"].as_u64().unwrap();
    let length = js["route_length"].as_f64().unwrap();
    assert!(has_route, "state_json reports the route is attached");
    assert_eq!(waypoints, 3, "the 3-stop L route carries 3 waypoints");
    assert!(length > 0.0, "the route has non-zero design-unit length (got {length})");

    // (2) Following: seek t from 0 → 1 and watch the camera TARGET advance along the
    //     route. Distance travelled along the path is monotone non-decreasing.
    let mut follow = Map3D::from_ways("OSM 3D", &ways, 0.2).with_route(&ways, &route).with_route_follow(true);
    follow.set_clock(0.0); // pin the clock → deterministic, drive purely by seek
    let start = follow.route().unwrap().point_at(0.0);
    let dest = follow.route().unwrap().point_at(1.0);

    let mut prev_d = -1.0f32;
    let mut monotone = true;
    let mut targets = Vec::new();
    for k in 0..=10 {
        let t = k as f32 / 10.0;
        follow.update(facett_map3d::Msg::RouteProgress(t));
        let tg = cam_target(&follow);
        // Path distance ≈ straight-line distance from start (the L route only turns
        // once, so distance-from-start is monotone along it).
        let d = ((tg[0] - start[0]).powi(2) + (tg[2] - start[2]).powi(2)).sqrt();
        monotone &= d >= prev_d - 1e-4;
        prev_d = d;
        targets.push(tg);
    }
    // t=0 sits at the start, t=1 at the destination.
    follow.update(facett_map3d::Msg::RouteProgress(0.0));
    let at0 = cam_target(&follow);
    follow.update(facett_map3d::Msg::RouteProgress(1.0));
    let at1 = cam_target(&follow);
    let at_start = ((at0[0] - start[0]).powi(2) + (at0[2] - start[2]).powi(2)).sqrt() < 1e-3;
    let at_dest = ((at1[0] - dest[0]).powi(2) + (at1[2] - dest[2]).powi(2)).sqrt() < 1e-3;
    let advanced = ((at1[0] - at0[0]).powi(2) + (at1[2] - at0[2]).powi(2)).sqrt() > 0.1;
    assert!(monotone, "the follow camera target advances monotonically along the route");
    assert!(at_start && at_dest, "t=0 → start, t=1 → destination");
    assert!(advanced, "the target visibly moves from start to destination");

    // (3) The route BEARING turns across the L-corner (turn-by-turn heading changes).
    follow.update(facett_map3d::Msg::RouteProgress(0.1));
    let brg_leg1 = follow.state_json()["route_bearing_deg"].as_f64().unwrap();
    follow.update(facett_map3d::Msg::RouteProgress(0.9));
    let brg_leg2 = follow.state_json()["route_bearing_deg"].as_f64().unwrap();
    let turn = (brg_leg2 - brg_leg1).abs();
    let turns = turn.min(360.0 - turn) > 30.0;
    assert!(turns, "the follow bearing swings across the corner ({brg_leg1:.1}° → {brg_leg2:.1}°)");

    // (4) Determinism: same progress ⇒ identical camera target (pure function of t).
    follow.update(facett_map3d::Msg::RouteProgress(0.42));
    let d1 = cam_target(&follow);
    follow.update(facett_map3d::Msg::RouteProgress(0.42));
    let d2 = cam_target(&follow);
    let deterministic = d1 == d2;
    assert!(deterministic, "the follow pose is a pure function of progress");

    // (5) Follow OFF ⇒ the route does not drive the camera (a plain orbit target).
    let mut unfollowed = Map3D::from_ways("OSM 3D", &ways, 0.2).with_route(&ways, &route);
    unfollowed.set_clock(0.0);
    let before = cam_target(&unfollowed);
    unfollowed.update(facett_map3d::Msg::RouteProgress(1.0)); // seek with follow OFF
    let after = cam_target(&unfollowed);
    let follow_off_inert = before == after && !unfollowed.route_follow_active();
    assert!(follow_off_inert, "with follow OFF, seeking progress leaves the camera untouched");

    #[cfg(feature = "testmatrix")]
    {
        let ok = added_geom && has_route && waypoints == 3 && length > 0.0 && monotone && at_start && at_dest && advanced && turns && deterministic && follow_off_inert;
        facett_core::testmatrix::emit(
            "facett-map3d",
            "route_overlay_and_turn_by_turn_follow",
            ok,
            &format!(
                "tris_added={tris_added} has_route={has_route} waypoints={waypoints} len={length:.3} \
                 monotone={monotone} start={at_start} dest={at_dest} advanced={advanced} \
                 turn_deg={turn:.1} det={deterministic} follow_off_inert={follow_off_inert}"
            ),
        );
    }
}