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

+312
View File
@@ -0,0 +1,312 @@
use std::rc::Rc;
use gpui::{
AnyElement, App, Bounds, Hsla, IntoElement, PathBuilder, Pixels, Point, Styled, Window, canvas,
fill, size,
};
use super::{
LatLon, MapViewport,
geojson::{Feature, Geometry},
heat::density,
};
use crate::{
canvas::{at, finish, outline},
theme::{ActiveTheme, Palette},
};
/// What a layer paints on: where the map looks, the view's size and top left, the tile's side, and the theme's inks and measures in pixels.
struct Paper {
view: MapViewport,
size: (f32, f32),
origin: Point<Pixels>,
tile: f32,
colors: Palette,
route: Pixels,
line: Pixels,
dot: f32,
cell: f32,
reach: f32,
}
impl Paper {
fn ink(&self, hue: Option<usize>, owner: &str) -> Hsla {
match hue {
Some(hue) => self.colors.hue(hue, owner),
None => self.colors.accent,
}
}
/// The shifts by whole worlds that bring points spanning `x` into view.
fn copies(&self, points: &[(f32, f32)], pad: f32) -> Vec<f32> {
let least = points
.iter()
.map(|point| point.0)
.fold(f32::INFINITY, f32::min);
let most = points
.iter()
.map(|point| point.0)
.fold(f32::NEG_INFINITY, f32::max);
self.view
.copies((least - pad, most + pad), self.size.0, self.tile)
}
fn stroke(
&self,
points: &[(f32, f32)],
shift: f32,
width: Pixels,
ink: Hsla,
window: &mut Window,
) {
let shifted: Vec<_> = points.iter().map(|(x, y)| (x + shift, *y)).collect();
finish(outline(&shifted, self.origin, width, false), ink, window);
}
fn dot(&self, place: LatLon, ink: Hsla, window: &mut Window) {
let (x, y) = self.view.to_view(place, self.size, self.tile);
let half = self.dot / 2.0;
for shift in self.copies(&[(x, y)], half) {
let corner = at(self.origin, (x + shift - half, y - half));
let side = Pixels::from(self.dot);
window.paint_quad(
fill(Bounds::new(corner, size(side, side)), ink).corner_radii(side / 2.0),
);
}
}
/// An area's rings washed as one path, so holes stay open, and outlined.
fn area(&self, rings: &[Vec<LatLon>], ink: Hsla, window: &mut Window) {
let runs: Vec<Vec<(f32, f32)>> = rings
.iter()
.map(|ring| self.view.run(ring, self.size, self.tile))
.collect();
let Some(outer) = runs.first() else {
return;
};
for shift in self.copies(outer, 0.0) {
let mut path = PathBuilder::fill();
for run in &runs {
let mut points = run.iter().map(|(x, y)| at(self.origin, (x + shift, *y)));
if let Some(first) = points.next() {
path.move_to(first);
points.for_each(|next| path.line_to(next));
path.close();
}
}
finish(path, ink.alpha(0.25), window);
for run in &runs {
self.stroke(run, shift, self.line, ink, window);
}
}
}
fn line(&self, places: &[LatLon], width: Pixels, ink: Hsla, window: &mut Window) {
let run = self.view.run(places, self.size, self.tile);
for shift in self.copies(&run, 0.0) {
self.stroke(&run, shift, width, ink, window);
}
}
fn geometry(&self, geometry: &Geometry, ink: Hsla, window: &mut Window) {
match geometry {
Geometry::Point(place) => self.dot(*place, ink, window),
Geometry::MultiPoint(places) => places
.iter()
.for_each(|place| self.dot(*place, ink, window)),
Geometry::LineString(places) => self.line(places, self.line, ink, window),
Geometry::MultiLineString(lines) => lines
.iter()
.for_each(|places| self.line(places, self.line, ink, window)),
Geometry::Polygon(rings) => self.area(rings, ink, window),
Geometry::MultiPolygon(polygons) => polygons
.iter()
.for_each(|rings| self.area(rings, ink, window)),
Geometry::Collection(parts) => parts
.iter()
.for_each(|part| self.geometry(part, ink, window)),
}
}
}
/// Something the map draws over its tiles and under its pins.
#[derive(Clone)]
pub enum MapLayer {
Route(RouteLine),
Features(GeoJsonLayer),
Heat(GeoHeatmap),
}
impl MapLayer {
fn paint(&self, paper: &Paper, window: &mut Window) {
match self {
MapLayer::Route(route) => {
let ink = paper.ink(route.hue, "route");
paper.line(&route.places, paper.route, ink, window);
}
MapLayer::Features(layer) => {
let ink = paper.ink(layer.hue, "features");
for feature in layer.features.iter() {
if let Some(geometry) = &feature.geometry {
paper.geometry(geometry, ink, window);
}
}
}
MapLayer::Heat(heat) => {
let ink = paper.ink(heat.hue, "heat");
let mut points = Vec::new();
for (place, weight) in heat.points.iter() {
let (x, y) = paper.view.to_view(*place, paper.size, paper.tile);
for shift in paper.copies(&[(x, y)], paper.reach) {
points.push(((x + shift, y), *weight));
}
}
let heat = density(&points, paper.size, paper.cell, paper.reach);
for row in 0..heat.rows {
for column in 0..heat.columns {
let value = heat.at(column, row);
if value > 0.01 {
let corner = (column as f32 * heat.cell, row as f32 * heat.cell);
let side = Pixels::from(heat.cell);
let bounds = Bounds::new(at(paper.origin, corner), size(side, side));
window.paint_quad(fill(bounds, ink.alpha(value * 0.7)));
}
}
}
}
}
}
}
/// A line along places in order, as a route runs; it crosses the date line the short way.
#[derive(Clone)]
pub struct RouteLine {
places: Vec<LatLon>,
hue: Option<usize>,
}
impl RouteLine {
/// Fails with fewer than two places.
pub fn new(places: impl IntoIterator<Item = LatLon>) -> Self {
let places: Vec<LatLon> = places.into_iter().collect();
assert!(places.len() >= 2, "a route of {} place", places.len());
Self { places, hue: None }
}
/// The chart hue it wears; the accent without one.
pub fn hue(mut self, hue: usize) -> Self {
self.hue = Some(hue);
self
}
}
/// GeoJSON features in one hue: areas washed and outlined with their holes open, lines stroked, points dotted.
#[derive(Clone)]
pub struct GeoJsonLayer {
features: Rc<[Feature]>,
hue: Option<usize>,
}
impl GeoJsonLayer {
pub fn new(features: impl Into<Rc<[Feature]>>) -> Self {
Self {
features: features.into(),
hue: None,
}
}
pub fn hue(mut self, hue: usize) -> Self {
self.hue = Some(hue);
self
}
}
/// Weighted places as heat, hotter where they crowd, scaled to the densest spot in view.
#[derive(Clone)]
pub struct GeoHeatmap {
points: Rc<[(LatLon, f32)]>,
hue: Option<usize>,
}
impl GeoHeatmap {
pub fn new(points: impl Into<Rc<[(LatLon, f32)]>>) -> Self {
Self {
points: points.into(),
hue: None,
}
}
pub fn hue(mut self, hue: usize) -> Self {
self.hue = Some(hue);
self
}
}
impl From<RouteLine> for MapLayer {
fn from(route: RouteLine) -> Self {
MapLayer::Route(route)
}
}
impl From<GeoJsonLayer> for MapLayer {
fn from(layer: GeoJsonLayer) -> Self {
MapLayer::Features(layer)
}
}
impl From<GeoHeatmap> for MapLayer {
fn from(heat: GeoHeatmap) -> Self {
MapLayer::Heat(heat)
}
}
/// A canvas that paints `layers` in order over a view of `size`; none without layers.
pub(crate) fn painted(
layers: Vec<MapLayer>,
view: MapViewport,
size: (f32, f32),
tile: f32,
window: &Window,
cx: &App,
) -> Option<AnyElement> {
if layers.is_empty() {
return None;
}
let theme = cx.theme();
let (sizes, rem) = (theme.maps(), window.rem_size());
let (colors, pixels) = (theme.colors.clone(), |rems: gpui::Rems| rems.to_pixels(rem));
let (route, line, dot) = (
pixels(sizes.route),
pixels(sizes.line),
f32::from(pixels(sizes.dot)),
);
let (cell, reach) = (
f32::from(pixels(sizes.heat_cell)),
f32::from(pixels(sizes.heat_reach)),
);
Some(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
let paper = Paper {
view,
size,
origin: bounds.origin,
tile,
colors,
route,
line,
dot,
cell,
reach,
};
layers.iter().for_each(|layer| layer.paint(&paper, window));
},
)
.absolute()
.top_0()
.left_0()
.size_full()
.into_any_element(),
)
}