Maps · Layers: routes, GeoJSON, heat and clusters
MapView takes layers over its tiles and under its pins. RouteLine runs along places the short way across the date line. read_geojson reads RFC 7946 to features, strictly: a position past the globe, a line of one position or a ring that does not close fails with its reason, and a feature placed nowhere keeps no geometry. GeoJsonLayer washes areas as one path per polygon, so holes stay open, outlines them, strokes lines and dots points. GeoHeatmap spreads weighted places in a quartic kernel over small cells, scaled to the densest spot in view. Lines and areas unwrap across the date line and repeat on every world in view. cluster_markers gathers pins by squares of the world, so a pan keeps each group; a cluster shows its count, and its press shows its members whole, never farther out. A zoom that may regroup them hands a focused pin's or cluster's focus to the map.
This commit is contained in:
1 parent
91a0fc2252
commit
e3a96b04e0
16 files changed
+1350
-48
No files matched your search
@@ -0,0 +1,209 @@
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||
Pixels, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
MapViewport,
|
||||
geo::{project, unproject},
|
||||
marker::{MapMarker, OnMarker},
|
||||
view::OnMapViewport,
|
||||
};
|
||||
use crate::{
|
||||
primitives::{FocusRing, tab_stop},
|
||||
theme::{ActiveTheme, TextSize},
|
||||
};
|
||||
|
||||
/// Pins gathered by squares of the world `side` pixels wide at the view's zoom, so a pan keeps each group: each square that holds pins, keyed by its column and row, with its members in order.
|
||||
pub(crate) fn gather(points: &[(f64, f64)], side: f64) -> Vec<((i64, i64), Vec<usize>)> {
|
||||
assert!(side > 0.0, "a gathering square of {side}");
|
||||
let mut groups: Vec<((i64, i64), Vec<usize>)> = Vec::new();
|
||||
for (ix, (x, y)) in points.iter().enumerate() {
|
||||
let key = ((x / side).floor() as i64, (y / side).floor() as i64);
|
||||
match groups.iter_mut().find(|(each, _)| *each == key) {
|
||||
Some((_, members)) => members.push(ix),
|
||||
None => groups.push((key, vec![ix])),
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
/// The map's pins as drawn, and when `gathered`, the focus of each pin and cluster drawn.
|
||||
pub(crate) struct Pins {
|
||||
pub elements: Vec<AnyElement>,
|
||||
pub gathered: Vec<FocusHandle>,
|
||||
}
|
||||
|
||||
/// The pins a map lays: its markers, what hears a pin's press, and whether pins that crowd gather.
|
||||
pub(crate) struct Laying {
|
||||
pub markers: Vec<MapMarker>,
|
||||
pub on_marker: Option<OnMarker>,
|
||||
pub gathered: bool,
|
||||
}
|
||||
|
||||
impl Laying {
|
||||
/// Lays the pins at their places, or gathers pins that crowd one square of the world into a cluster that shows their count. Each keeps its own focus in view and out, and takes Tab and presses while in view; a cluster's press shows its members whole.
|
||||
pub(crate) fn lay(
|
||||
self,
|
||||
owner: &ElementId,
|
||||
(view, size, tile): (MapViewport, (f32, f32), f32),
|
||||
set: &OnMapViewport,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Pins {
|
||||
let Laying {
|
||||
markers,
|
||||
on_marker,
|
||||
gathered,
|
||||
} = self;
|
||||
let seen = |(x, y): (f32, f32)| (0.0..=size.0).contains(&x) && (0.0..=size.1).contains(&y);
|
||||
let rem = window.rem_size();
|
||||
let sizes = cx.theme().maps();
|
||||
let groups = match gathered {
|
||||
true => {
|
||||
let side = f64::from(tile) * view.zoom.exp2();
|
||||
let points: Vec<(f64, f64)> = markers
|
||||
.iter()
|
||||
.map(|marker| {
|
||||
let (x, y) = project(marker.at);
|
||||
(x * side, y * side)
|
||||
})
|
||||
.collect();
|
||||
gather(&points, f64::from(sizes.gather.to_pixels(rem)))
|
||||
}
|
||||
false => (0..markers.len())
|
||||
.map(|ix| ((0, ix as i64), vec![ix]))
|
||||
.collect(),
|
||||
};
|
||||
let mut pins = Pins {
|
||||
elements: Vec::new(),
|
||||
gathered: Vec::new(),
|
||||
};
|
||||
for (key, members) in groups {
|
||||
if let [only] = members[..] {
|
||||
let marker = markers[only].clone();
|
||||
let at = view.to_view(marker.at, size, tile);
|
||||
let press = on_marker.clone().map(|on_marker| {
|
||||
let key = (owner.clone(), format!("pin-{}", marker.key));
|
||||
let focus = tab_stop(key.into(), seen(at), window, cx);
|
||||
if gathered {
|
||||
pins.gathered.push(focus.clone());
|
||||
}
|
||||
(focus, on_marker)
|
||||
});
|
||||
pins.elements
|
||||
.push(marker.place(owner, at, press, seen(at), cx));
|
||||
continue;
|
||||
}
|
||||
let places: Vec<_> = members.iter().map(|ix| markers[*ix].at).collect();
|
||||
let mean =
|
||||
places
|
||||
.iter()
|
||||
.map(|place| project(*place))
|
||||
.fold((0.0, 0.0), |sum, (x, y)| {
|
||||
(
|
||||
sum.0 + x / places.len() as f64,
|
||||
sum.1 + y / places.len() as f64,
|
||||
)
|
||||
});
|
||||
let at = view.to_view(unproject(mean), size, tile);
|
||||
let id = (owner.clone(), format!("cluster-{}-{}", key.0, key.1));
|
||||
let focus = tab_stop(id.clone().into(), seen(at), window, cx);
|
||||
pins.gathered.push(focus.clone());
|
||||
let margin = f32::from(sizes.margin.to_pixels(rem));
|
||||
let set = set.clone();
|
||||
let fit = move |window: &mut Window, cx: &mut App| {
|
||||
set(view.fitting(&places, size, tile, margin * 4.0), window, cx)
|
||||
};
|
||||
pins.elements.push(bubble(
|
||||
id.into(),
|
||||
members.len(),
|
||||
at,
|
||||
focus,
|
||||
seen(at),
|
||||
fit,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
pins
|
||||
}
|
||||
}
|
||||
|
||||
/// A round cluster at a view point, its count inside, pressable while `seen`.
|
||||
fn bubble(
|
||||
id: ElementId,
|
||||
count: usize,
|
||||
(x, y): (f32, f32),
|
||||
focus: FocusHandle,
|
||||
seen: bool,
|
||||
fit: impl Fn(&mut Window, &mut App) + 'static,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let side = theme.maps().cluster;
|
||||
div()
|
||||
.absolute()
|
||||
.left(Pixels::from(x))
|
||||
.top(Pixels::from(y))
|
||||
.size_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.id(id)
|
||||
.track_focus(&focus)
|
||||
.flex_none()
|
||||
.size(side)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(theme.colors.accent)
|
||||
.border_1()
|
||||
.border_color(theme.colors.on_accent)
|
||||
.focus_ring(cx)
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.on_accent)
|
||||
.child(count.to_string())
|
||||
.when(seen, |bubble| {
|
||||
bubble
|
||||
.cursor_pointer()
|
||||
.on_click(move |_, window, cx| fit(window, cx))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::gather;
|
||||
|
||||
#[test]
|
||||
fn pins_in_one_square_of_the_world_gather() {
|
||||
let points = [
|
||||
(10.0, 10.0),
|
||||
(50.0, 20.0),
|
||||
(70.0, 10.0),
|
||||
(130.0, 10.0),
|
||||
(20.0, 55.0),
|
||||
(20.0, 65.0),
|
||||
];
|
||||
assert_eq!(
|
||||
gather(&points, 60.0),
|
||||
vec![
|
||||
((0, 0), vec![0, 1, 4]),
|
||||
((1, 0), vec![2]),
|
||||
((2, 0), vec![3]),
|
||||
((0, 1), vec![5])
|
||||
],
|
||||
"each square keeps its members in order, the squares in the order first met"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "a gathering square of 0")]
|
||||
fn a_square_of_nothing_fails() {
|
||||
gather(&[(0.0, 0.0)], 0.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user