use super::*;
use crate::legibility::FrameOpts;
use egui::{Pos2, Rect, Vec2};
fn mercator(lon: f64, lat: f64) -> (f64, f64) {
let x = (lon + 180.0) / 360.0;
let lat_r = lat.to_radians();
let y = (1.0 - (lat_r.tan() + 1.0 / lat_r.cos()).ln() / std::f64::consts::PI) / 2.0;
(x, y)
}
fn viewport() -> Rect {
Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0))
}
fn swedish_cities() -> Vec<[f64; 2]> {
vec![
[13.0007, 55.6050], [11.9746, 57.7089], [18.0686, 59.3293], [20.2630, 63.8258], [20.2253, 67.8558], ]
}
#[test]
fn uc1_osm_2d_mercator_flat() {
let pos = Projected::new(swedish_cities(), "mercator", mercator);
let elev = Flat;
let hier = Flatten::new(pos.feature_count());
let layer = LayerRef::new(1, &pos, &elev, &hier);
let (sx, sy) = mercator(18.0686, 59.3293);
let view = View {
centre: WorldPos::flat(sx, sy),
scale: 6000.0,
viewport: viewport(),
epoch: 1,
};
let mut cache = EngineCache::default();
let plan = plan(&[layer], &view, FrameOpts::default(), &mut cache);
assert_eq!(plan.layers.len(), 1);
assert!(plan.visible() > 0, "a country view must see cities");
let l = &plan.layers[0];
let i = l.visible.iter().position(|&f| f == 2).expect("Stockholm culled out of its own view");
let c = view.viewport.center();
assert!((l.screen[i] - c).length() < 0.01, "the eye feature must land at the viewport centre");
assert!(plan.layers[0].elevation.iter().all(|&z| z == 0.0), "use case 1 is flat");
}
#[test]
fn uc1_mercator_north_is_up_and_the_engine_did_not_flip_it() {
let pos = Projected::new(swedish_cities(), "mercator", mercator);
let (elev, hier) = (Flat, Flatten::new(5));
let layer = LayerRef::new(1, &pos, &elev, &hier);
let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 2000.0, viewport: viewport(), epoch: 1 };
let mut cache = EngineCache::default();
let plan = plan(&[layer], &view, FrameOpts::default(), &mut cache);
let l = &plan.layers[0];
let y_of = |f: u32| l.visible.iter().position(|&x| x == f).map(|i| l.screen[i].y);
if let (Some(kiruna), Some(malmo)) = (y_of(4), y_of(0)) {
assert!(kiruna < malmo, "Kiruna (67.8N) must be above Malmö (55.6N): {kiruna} vs {malmo}");
}
}
#[test]
fn uc2_osm_3d_is_uc1_with_one_trait_swapped() {
let pos = Projected::new(swedish_cities(), "mercator", mercator);
let hier = Flatten::new(5);
let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 2000.0, viewport: viewport(), epoch: 1 };
let flat = Flat;
let mut c1 = EngineCache::default();
let p2d = plan(&[LayerRef::new(1, &pos, &flat, &hier)], &view, FrameOpts::default(), &mut c1);
let terrain = Sampled::new("terrain", (0.0, 500.0), |x, y| ((x * 97.0).sin() + (y * 61.0).cos()) * 250.0 + 250.0);
let mut c2 = EngineCache::default();
let p3d = plan(&[LayerRef::new(1, &pos, &terrain, &hier)], &view, FrameOpts::default(), &mut c2);
assert_eq!(p2d.layers[0].visible, p3d.layers[0].visible, "the same features must be visible");
assert_eq!(p2d.layers[0].screen, p3d.layers[0].screen, "ground-plane projection must be identical");
assert!(p2d.layers[0].elevation.iter().all(|&z| z == 0.0));
assert!(
p3d.layers[0].elevation.iter().any(|&z| z > 0.0),
"the terrain source must actually raise something — otherwise the test proves nothing"
);
}
#[test]
fn uc3_flat_overlay_identity_is_screen_space() {
let pos = Identity::from_f32(&[(100.0, 100.0), (400.0, 300.0), (700.0, 500.0)]);
let (elev, hier) = (Flat, Flatten::new(3));
let view = View {
centre: WorldPos::flat(400.0, 300.0),
scale: 1.0,
viewport: viewport(),
epoch: 1,
};
let mut cache = EngineCache::default();
let plan = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
let l = &plan.layers[0];
for (i, &f) in l.visible.iter().enumerate() {
let want = match f {
0 => Pos2::new(100.0, 100.0),
1 => Pos2::new(400.0, 300.0),
_ => Pos2::new(700.0, 500.0),
};
assert!((l.screen[i] - want).length() < 0.001, "identity source must be pixel-for-pixel");
}
}
fn ring_layout(n: usize, r: f64) -> Vec<[f64; 2]> {
(0..n)
.map(|i| {
let a = i as f64 / n as f64 * std::f64::consts::TAU;
[a.cos() * r, a.sin() * r]
})
.collect()
}
#[test]
fn uc4_graph_2d_layout_flat_with_edges_and_labels() {
let pos = Layout::new(ring_layout(60, 200.0));
let (elev, hier) = (Flat, Flatten::new(60));
let edges: Vec<(u32, u32)> = (0..60u32).map(|i| (i, (i + 1) % 60)).collect();
let sizes = vec![Vec2::new(40.0, 12.0); 60];
let prio: Vec<f32> = (0..60).map(|i| i as f32).collect();
let layer = LayerRef {
edges: &edges,
label_size: &sizes,
label_priority: &prio,
..LayerRef::new(1, &pos, &elev, &hier)
};
let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.5, viewport: viewport(), epoch: 1 };
let mut cache = EngineCache::default();
let p = plan(&[layer], &view, FrameOpts::legible(), &mut cache);
assert_eq!(p.layers[0].visible.len(), 60, "the whole ring fits the viewport");
let labels = p.frame.labels.as_ref().expect("legible() must produce a label placement");
assert!(labels.placed.len() > 0, "some labels must be placed");
assert!(p.frame.report.edges_considered > 0, "edges must reach the thinner");
}
#[test]
fn uc5_graph_3d_is_uc4_with_a_metric_elevation() {
let pos = Layout::new(ring_layout(60, 200.0));
let hier = Flatten::new(60);
let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.5, viewport: viewport(), epoch: 1 };
let flat = Flat;
let mut c1 = EngineCache::default();
let p2 = plan(&[LayerRef::new(1, &pos, &flat, &hier)], &view, FrameOpts::default(), &mut c1);
let centrality: Vec<f32> = (0..60).map(|i| (i as f32 % 7.0) * 3.0).collect();
let metric = Metric::new(centrality, 2.0);
assert_eq!(metric.z_range(), (0.0, 36.0), "the metric must declare its real range");
let mut c2 = EngineCache::default();
let p3 = plan(&[LayerRef::new(1, &pos, &metric, &hier)], &view, FrameOpts::default(), &mut c2);
assert_eq!(p2.layers[0].screen, p3.layers[0].screen, "ground projection unchanged by elevation");
assert!(p3.layers[0].elevation.iter().any(|&z| z > 0.0));
}
#[test]
fn uc6_hierarchy_drill_is_the_same_operation_as_a_map_zoom() {
let bands = Bands::new(
vec![
Band { members: vec![0], min_scale: 0.0, children: [(0u32, vec![1, 2, 3])].into_iter().collect() },
Band {
members: vec![1, 2, 3],
min_scale: 10.0,
children: [(1u32, vec![4, 5, 6]), (2, vec![7, 8, 9]), (3, vec![10, 11, 12])].into_iter().collect(),
},
Band { members: (4..=12).collect(), min_scale: 100.0, children: Default::default() },
],
"mimir-code",
);
assert_eq!(bands.depth(), 3);
assert_eq!(bands.level_for(1.0), 0, "zoomed out = the repo node");
assert_eq!(bands.level_for(50.0), 1, "mid = the crates");
assert_eq!(bands.level_for(500.0), 2, "zoomed in = the files");
assert_eq!(bands.expand(0, 0), &[1, 2, 3], "one step deeper swaps coarse for fine");
assert_eq!(bands.parent_of(2, 5), Some(1), "zooming out must carry the selection up");
let pts: Vec<[f64; 2]> = (0..13).map(|i| [(i % 4) as f64 * 50.0, (i / 4) as f64 * 50.0]).collect();
let pos = Layout::new(pts);
let risk: Vec<f32> = (0..13).map(|i| (i as f32) * 0.5).collect();
let elev = Metric::new(risk, 4.0);
let mut counts = Vec::new();
for scale in [1.0, 50.0, 500.0] {
let view = View {
centre: WorldPos::flat(75.0, 75.0),
scale,
viewport: Rect::from_min_size(Pos2::ZERO, Vec2::new(100_000.0, 100_000.0)),
epoch: 1,
};
let mut cache = EngineCache::default();
let p = plan(&[LayerRef::new(1, &pos, &elev, &bands)], &view, FrameOpts::default(), &mut cache);
counts.push((p.layers[0].level, p.layers[0].visible.len()));
}
assert_eq!(counts, vec![(0, 1), (1, 3), (2, 9)], "descending a level must swap coarse instances for fine");
}
#[test]
fn uc7_graph_over_map_two_position_sources_in_one_frame() {
let map_pos = Projected::new(swedish_cities(), "mercator", mercator);
let map_elev = Flat;
let map_hier = Flatten::new(5);
let g_pos = Projected::new(
vec![[13.0007, 55.6050], [18.0686, 59.3293], [20.2630, 63.8258]],
"mercator",
mercator,
);
let g_elev = Metric::new(vec![10.0, 40.0, 25.0], 1.0);
let g_hier = Flatten::new(3);
let g_edges = [(0u32, 1u32), (1, 2)];
let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 3000.0, viewport: viewport(), epoch: 1 };
let mut cache = EngineCache::default();
let p = plan(
&[
LayerRef::new(1, &map_pos, &map_elev, &map_hier),
LayerRef { edges: &g_edges, pick_shape: PickShape::Radius(14.0), ..LayerRef::new(2, &g_pos, &g_elev, &g_hier) },
],
&view,
FrameOpts::legible(),
&mut cache,
);
assert_eq!(p.layers.len(), 2, "both layers must survive the frame");
assert_eq!(p.pick.layer_count(), 2, "one pick index, both layers in it");
assert!(p.layers[1].elevation.iter().any(|&z| z > 0.0), "the graph layer keeps its metric height");
assert!(p.layers[0].elevation.iter().all(|&z| z == 0.0), "the map layer stays flat");
let map_sthlm = p.layers[0]
.visible
.iter()
.position(|&f| f == 2)
.map(|i| p.layers[0].screen[i])
.expect("map layer lost Stockholm");
let graph_sthlm = p.layers[1]
.visible
.iter()
.position(|&f| f == 1)
.map(|i| p.layers[1].screen[i])
.expect("graph layer lost Stockholm");
assert!(
(map_sthlm - graph_sthlm).length() < 0.01,
"a graph node ON geography must land on its city: {map_sthlm:?} vs {graph_sthlm:?}"
);
let hit = p.pick.pick(graph_sthlm);
assert_eq!(hit.id.layer(), 2, "the upper layer must win the contested pixel");
assert_eq!(hit.id.feature(), 1);
}
#[test]
fn oversize_features_are_forced_in_exactly_once() {
#[derive(Debug)]
struct Wide(Vec<[f64; 2]>);
impl PositionSource for Wide {
fn feature_count(&self) -> usize {
self.0.len()
}
fn position(&self, id: u32) -> WorldPos {
self.0.get(id as usize).map_or(WorldPos::new(f64::NAN, f64::NAN, f64::NAN), |&[x, y]| WorldPos::flat(x, y))
}
fn bounds(&self, id: u32) -> WorldAabb {
if id == 0 {
WorldAabb { min: WorldPos::flat(0.0, 0.0), max: WorldPos::flat(1000.0, 1000.0) }
} else {
WorldAabb::point(self.position(id))
}
}
fn kind(&self) -> &'static str {
"wide"
}
}
let pos = Wide((0..200).map(|i| [i as f64 * 5.0, i as f64 * 5.0]).collect());
let (elev, hier) = (Flat, Flatten::new(200));
let view = View { centre: WorldPos::flat(500.0, 500.0), scale: 1.0, viewport: viewport(), epoch: 1 };
let mut cache = EngineCache::default();
let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
let l = &p.layers[0];
assert!(l.visible.contains(&0), "the dataset-spanning feature must be drawn");
assert_eq!(l.visible.iter().filter(|&&f| f == 0).count(), 1, "and exactly once");
}
#[test]
fn the_cull_index_is_reused_across_frames_when_nothing_moved() {
let pos = Layout::new(ring_layout(500, 300.0));
let (elev, hier) = (Flat, Flatten::new(500));
let mut cache = EngineCache::default();
for epoch in 0..5u64 {
let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.0, viewport: viewport(), epoch };
let _ = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
}
assert_eq!(cache.len(), 1, "a static layer must build its index once, not once per frame");
}
#[test]
fn empty_everything_plans_a_frame_without_panicking() {
let pos = Identity::new(Vec::new());
let (elev, hier) = (Flat, Flatten::new(0));
let view = View::default();
let mut cache = EngineCache::default();
let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::legible(), &mut cache);
assert_eq!(p.visible(), 0);
assert!(p.pick.pick(Pos2::ZERO).id.is_nothing());
}
#[test]
fn state_json_publishes_the_observables_a_robot_asserts_on() {
let pos = Layout::new(ring_layout(20, 100.0));
let (elev, hier) = (Flat, Flatten::new(20));
let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 2.0, viewport: viewport(), epoch: 3 };
let mut cache = EngineCache::default();
let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::legible(), &mut cache);
let j = p.state_json();
assert_eq!(j["layers"][0]["visible"], 20);
assert_eq!(j["pick_layers"], 1);
assert!(j["layers"][0]["cull"]["candidates"].is_number());
}