From 11fbb07ff806e7de30d3d1f967914e14f6b8d75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 27 Sep 2026 22:57:05 -0400 Subject: [PATCH] =?UTF-8?q?Maps=20=C2=B7=20Layers:=20tests=20hold=20what?= =?UTF-8?q?=20clusters=20promise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests hold that a pan keeps a focused cluster, that a cluster away from the view takes no Tab, that a zoom out folding a focused pin hands its focus to the map, and that a press shows members far apart whole. The heat test pins the quartic kernel's value halfway out; a ring of fewer than four positions fails with its own reason, and has a test. Co-Authored-By: Claude Opus 5.5 --- src/maps/geojson.rs | 12 ++++- src/maps/heat.rs | 9 ++-- src/maps/tests/layers.rs | 96 ++++++++++++++++++++++++++++++++++++++++ src/maps/tests/mod.rs | 2 +- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/maps/geojson.rs b/src/maps/geojson.rs index d443f6b..6b019e0 100644 --- a/src/maps/geojson.rs +++ b/src/maps/geojson.rs @@ -141,8 +141,14 @@ fn line(json: &Value) -> Result> { fn ring(json: &Value) -> Result> { let ring = list(json, position)?; - if ring.len() < 4 || ring.first() != ring.last() { - bail!("a ring of {} positions that does not close", ring.len()); + if ring.len() < 4 { + bail!( + "a ring of {} positions; a ring holds four or more", + ring.len() + ); + } + if ring.first() != ring.last() { + bail!("a ring that does not close"); } Ok(ring) } @@ -211,6 +217,8 @@ mod tests { assert!(reason(r#"{"type": "LineString", "coordinates": [[0, 0]]}"#).contains("line of 1")); let open = r#"{"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1]]]}"#; assert!(reason(open).contains("does not close")); + let short = r#"{"type": "Polygon", "coordinates": [[[0,0],[1,1],[0,0]]]}"#; + assert!(reason(short).contains("four or more")); let named = r#"{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point"}}]}"#; let named = reason(named); assert!( diff --git a/src/maps/heat.rs b/src/maps/heat.rs index 345f5ec..d8dab76 100644 --- a/src/maps/heat.rs +++ b/src/maps/heat.rs @@ -13,7 +13,7 @@ impl Heat { } } -/// Each weighted view point spreads a quartic kernel `reach` pixels wide over cells `cell` pixels square across a view of `size`; the sum is scaled so its densest cell is 1. Fails on a negative or non-finite weight. +/// Each weighted view point spreads a quartic kernel reaching `reach` pixels from it over cells `cell` pixels square across a view of `size`; the sum is scaled so its densest cell is 1. Fails on a negative or non-finite weight. pub(crate) fn density( points: &[((f32, f32), f32)], (w, h): (f32, f32), @@ -74,9 +74,10 @@ mod tests { 1.0, "the cell under the point is the densest" ); - assert!( - heat.at(3, 2) > 0.0 && heat.at(3, 2) < 1.0, - "it falls off beside" + assert_eq!( + heat.at(3, 2), + 0.5625, + "halfway out a quartic kernel keeps (1 - 1/4) squared" ); assert_eq!(heat.at(6, 2), 0.0, "past its reach nothing"); } diff --git a/src/maps/tests/layers.rs b/src/maps/tests/layers.rs index 2b672cd..2bc07e4 100644 --- a/src/maps/tests/layers.rs +++ b/src/maps/tests/layers.rs @@ -107,3 +107,99 @@ fn every_layer_paints_over_repeated_worlds(cx: &mut TestAppContext) { fn a_route_of_one_place_fails() { RouteLine::new([LatLon::new(0.0, 0.0)]); } + +/// A pan moves the view, not the world's squares: the focused cluster keeps its focus and Enter still presses it. +#[gpui::test] +fn a_pan_keeps_a_focused_cluster(cx: &mut TestAppContext) { + let (host, cx) = stage(crowd, cx); + focus_map(cx); + press("tab", cx); + press("left", cx); + press("enter", cx); + assert!( + last(&host, cx).zoom > europe().zoom, + "the cluster kept focus through the pan" + ); +} + +fn far_crowd(_: &Stage, owner: Entity) -> AnyElement { + MapView::new("map", europe()) + .markers([ + MapMarker::new("tokyo", LatLon::new(35.7, 139.7)), + MapMarker::new("chiba", LatLon::new(35.9, 140.0)), + ]) + .cluster_markers() + .on_marker(|_, _, _| {}) + .on_viewport(move |view, _, cx| hear(&owner, view, cx)) + .into_any_element() +} + +/// A cluster whose place lies outside the view takes no Tab, as a pin there takes none: Tab reaches the zoom button. +#[gpui::test] +fn a_cluster_away_from_the_view_takes_no_tab(cx: &mut TestAppContext) { + let (host, cx) = stage(far_crowd, cx); + focus_map(cx); + press("tab", cx); + press("enter", cx); + assert_eq!(last(&host, cx).zoom, 3.0, "Enter on the zoom button"); +} + +fn folding(_: &Stage, owner: Entity) -> AnyElement { + let near = CROWD.map(|(key, lat, lon)| MapMarker::new(key, LatLon::new(lat, lon))); + MapView::new("map", MapViewport::new(LatLon::new(45.0, 0.0), 3.0)) + .markers( + near.into_iter() + .chain([MapMarker::new("rome", LatLon::new(41.9, 12.5))]), + ) + .cluster_markers() + .on_marker(|_, _, _| {}) + .on_viewport(move |view, _, cx| hear(&owner, view, cx)) + .into_any_element() +} + +/// At zoom 3 Rome stands alone; a zoom out folds it into the capitals' cluster, and its focus goes to the map. +#[gpui::test] +fn a_zoom_out_folding_a_focused_pin_hands_focus_to_the_map(cx: &mut TestAppContext) { + let (host, cx) = stage(folding, cx); + focus_map(cx); + press("tab", cx); + press("tab", cx); + press("-", cx); + assert_eq!(last(&host, cx).zoom, 2.0); + let before = moves(&host, cx); + press("left", cx); + assert_eq!(moves(&host, cx), before + 1, "the map holds focus"); +} + +const APART: [(f64, f64); 2] = [(20.0, -2.5), (21.0, 16.5)]; + +fn apart(_: &Stage, owner: Entity) -> AnyElement { + MapView::new("map", MapViewport::new(LatLon::new(20.0, 7.0), 2.0)) + .markers( + APART.iter().enumerate().map(|(ix, (lat, lon))| { + MapMarker::new(format!("far-{ix}"), LatLon::new(*lat, *lon)) + }), + ) + .cluster_markers() + .on_marker(|_, _, _| {}) + .on_viewport(move |view, _, cx| hear(&owner, view, cx)) + .into_any_element() +} + +/// Two pins 19 degrees apart in one square: the cluster's press shows both, which no fit of one alone would. +#[gpui::test] +fn a_clusters_press_shows_far_members_whole(cx: &mut TestAppContext) { + let (host, cx) = stage(apart, cx); + focus_map(cx); + press("tab", cx); + press("enter", cx); + let fit = last(&host, cx); + assert!(fit.zoom > 2.0, "nearer in: {}", fit.zoom); + for (lat, lon) in APART { + let (x, y) = fit.to_view(LatLon::new(lat, lon), SIZE, TILE); + assert!( + (0.0..=SIZE.0).contains(&x) && (0.0..=SIZE.1).contains(&y), + "{lat}, {lon} at {x}, {y}" + ); + } +} diff --git a/src/maps/tests/mod.rs b/src/maps/tests/mod.rs index e3b7390..1f5e1ec 100644 --- a/src/maps/tests/mod.rs +++ b/src/maps/tests/mod.rs @@ -12,10 +12,10 @@ use crate::{ theme::Theme, }; -/// The view inside the map's 1px border, in pixels. mod layers; mod popup; +/// The view inside the map's 1px border, in pixels. const SIZE: (f32, f32) = (398.0, 298.0); const TILE: f32 = 256.0;