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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 22:35:40 -04:00
1 parent f5909f6e43
commit a3115b8126
16 files changed
+1350 -48

No files matched your search

+62 -36
View File
@@ -1,13 +1,15 @@
use std::{collections::HashSet, f64::consts::LOG2_E, rc::Rc};
use gpui::{
AnyElement, App, AppContext as _, Bounds, ElementId, EmptyView, Entity, EntityId, ImageSource,
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window, canvas, div,
AnyElement, App, AppContext as _, Bounds, ElementId, EmptyView, Entity, EntityId, FocusHandle,
ImageSource, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, canvas, div,
};
use super::{
MapViewport, Tile,
cluster::Laying,
layer::{MapLayer, painted},
marker::{MapMarker, MapPopup, OnMarker},
tile::laid_tile,
};
@@ -28,12 +30,13 @@ struct Pan {
owner: EntityId,
}
/// The map's box, the pointer and viewport at the press of a pan under way, and the focus a shown popup took.
/// The map's box, the pointer and viewport at the press of a pan under way, the focus a shown popup took, and the focus of each pin and cluster a zoom may regroup.
#[derive(Default)]
struct Stage {
bounds: Bounds<Pixels>,
grip: Option<(Point<Pixels>, MapViewport)>,
takeover: Option<Entity<Takeover>>,
gathered: Vec<FocusHandle>,
}
/// A map of tiles the host supplies by zoom, column and row, with pins and a popup over them; a pin that holds focus keeps it off view, but takes Tab and presses only while its place is in view. A drag or the wheel pans it; Command or Control with the wheel zooms about the pointer; focused, the arrows pan and + and - zoom, as its buttons do. The owner keeps the viewport; a new one from the owner shows at once. It fills its box; the host gives it a height.
@@ -44,7 +47,9 @@ pub struct MapView {
tiles: Option<TileSource>,
zooms: (u8, u8),
attribution: Option<SharedString>,
layers: Vec<MapLayer>,
markers: Vec<MapMarker>,
gathered: bool,
on_marker: Option<OnMarker>,
popup: Option<MapPopup>,
pub(crate) on_viewport: Option<OnMapViewport>,
@@ -58,7 +63,9 @@ impl MapView {
tiles: None,
zooms: (0, MapViewport::ZOOMS.1 as u8),
attribution: None,
layers: Vec::new(),
markers: Vec::new(),
gathered: false,
on_marker: None,
popup: None,
on_viewport: None,
@@ -87,6 +94,18 @@ impl MapView {
self
}
/// A route, GeoJSON features or heat over the tiles and under the pins, drawn in the order added.
pub fn layer(mut self, layer: impl Into<MapLayer>) -> Self {
self.layers.push(layer.into());
self
}
/// Gathers pins that crowd one square of the world into a cluster that shows their count; a cluster's press shows its members whole. A zoom that regroups them hands a focused pin's or cluster's focus to the map.
pub fn cluster_markers(mut self) -> Self {
self.gathered = true;
self
}
/// Pins keyed by their places; two with one key fail, as they would share a focus.
pub fn markers(mut self, markers: impl IntoIterator<Item = MapMarker>) -> Self {
self.markers = markers.into_iter().collect();
@@ -146,6 +165,35 @@ impl RenderOnce for MapView {
let view = local.read(cx).value;
let size = sized(stage.read(cx).bounds);
let tile = f32::from(cx.theme().maps().tile);
let set: OnMapViewport = {
let (local, on_viewport) = (local.clone(), self.on_viewport.clone());
let (regrouped, map) = (stage.clone(), focus.clone());
Rc::new(move |next, window, cx| {
log::debug!(
"map: {:.4}, {:.4} at zoom {:.2}",
next.center.lat,
next.center.lon,
next.zoom
);
let zoomed = next.zoom != local.read(cx).value.zoom;
let held = regrouped
.read(cx)
.gathered
.iter()
.any(|pin| pin.is_focused(window));
if zoomed && held {
log::info!("map: a zoom regroups the pins; focus goes to the map");
window.focus(&map);
}
local.update(cx, |local, cx| {
local.value = next;
cx.notify();
});
if let Some(on_viewport) = &on_viewport {
on_viewport(next, window, cx);
}
})
};
let tiles: Vec<AnyElement> = match &self.tiles {
Some(source) => view
.tiles(size, tile, self.zooms)
@@ -154,19 +202,14 @@ impl RenderOnce for MapView {
.collect(),
None => Vec::new(),
};
let markers: Vec<AnyElement> = self
.markers
.into_iter()
.map(|marker| {
let at = view.to_view(marker.at, size, tile);
let seen = inside(at, size);
let press = self.on_marker.clone().map(|on_marker| {
let key = (id.clone(), format!("pin-{}", marker.key));
(tab_stop(key.into(), seen, window, cx), on_marker)
});
marker.place(&id, at, press, seen, cx)
})
.collect();
let layers = painted(self.layers, view, size, tile, window, cx);
let pins = Laying {
markers: self.markers,
on_marker: self.on_marker,
gathered: self.gathered,
}
.lay(&id, (view, size, tile), &set, window, cx);
stage.update(cx, |stage, _| stage.gathered = pins.gathered);
let shown = self
.popup
.map(|popup| (view.to_view(popup.at, size, tile), popup))
@@ -188,24 +231,6 @@ impl RenderOnce for MapView {
};
let theme = cx.theme();
let colors = theme.colors.clone();
let set: OnMapViewport = {
let (local, on_viewport) = (local.clone(), self.on_viewport.clone());
Rc::new(move |next, window, cx| {
log::debug!(
"map: {:.4}, {:.4} at zoom {:.2}",
next.center.lat,
next.center.lon,
next.zoom
);
local.update(cx, |local, cx| {
local.value = next;
cx.notify();
});
if let Some(on_viewport) = &on_viewport {
on_viewport(next, window, cx);
}
})
};
let now: Now = {
let (local, stage) = (local.clone(), stage.clone());
Rc::new(move |cx| (local.read(cx).value, sized(stage.read(cx).bounds)))
@@ -356,7 +381,8 @@ impl RenderOnce for MapView {
})
.on_drop(move |_: &Pan, _, cx| dropped.update(cx, |stage, _| stage.grip = None))
.children(tiles)
.children(markers)
.children(layers)
.children(pins.elements)
.child(controls)
.children(attribution)
.children(popup)