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
@@ -214,8 +214,8 @@ pub fn stream(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
),
|
||||
];
|
||||
section(
|
||||
"EventStream · LogStream → terminal::LogViewer · TraceWaterfall → debug::TimelineProfiler · SystemMonitor → devtools::ResourceMonitor · ProcessTable → terminal::ProcessList · NetworkGraph (throughput) → charts::RealtimeChart · MapView / GeoHeatmap → maps (T41) · WorldMap → maps (T41)",
|
||||
"Events as they come, newest first, kept by kind, with Pause to hold the list while new ones count up. Logs, traces, a machine's readings, its processes and live throughput have their homes in the terminal, agent, dev tools and charts chapters; the maps come with the maps chapter.",
|
||||
"EventStream · LogStream → terminal::LogViewer · TraceWaterfall → debug::TimelineProfiler · SystemMonitor → devtools::ResourceMonitor · ProcessTable → terminal::ProcessList · NetworkGraph (throughput) → charts::RealtimeChart · MapView / GeoHeatmap → maps::MapView, maps::GeoHeatmap · WorldMap → maps (T41c)",
|
||||
"Events as they come, newest first, kept by kind, with Pause to hold the list while new ones count up. Logs, traces, a machine's readings, its processes and live throughput have their homes in the terminal, agent, dev tools and charts chapters; the maps live in the maps chapter.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(620.)).child(EventStream::new("dashboard-events", events, now())))
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
use ely_gpui_component::maps::{
|
||||
GeoHeatmap, GeoJsonLayer, LatLon, MapMarker, MapView, MapViewport, RouteLine, read_geojson,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
|
||||
|
||||
use super::{ATTRIBUTION, tile};
|
||||
use crate::probe::probe;
|
||||
use crate::ui::{noise, section};
|
||||
|
||||
const ROUTE: [(f64, f64); 4] = [
|
||||
(51.5072, -0.1276),
|
||||
(64.1466, -21.9426),
|
||||
(44.6488, -63.5752),
|
||||
(40.7128, -74.006),
|
||||
];
|
||||
|
||||
const SHAPES: &str = r#"{"type": "FeatureCollection", "features": [
|
||||
{"type": "Feature", "properties": {"name": "The Bermuda Triangle"},
|
||||
"geometry": {"type": "Polygon", "coordinates": [
|
||||
[[-80.19, 25.76], [-64.78, 32.3], [-66.1, 18.47], [-80.19, 25.76]]]}},
|
||||
{"type": "Feature", "properties": {"name": "A ring around the Azores"},
|
||||
"geometry": {"type": "Polygon", "coordinates": [
|
||||
[[-35, 33], [-20, 33], [-20, 42], [-35, 42], [-35, 33]],
|
||||
[[-30, 36], [-25, 36], [-25, 39], [-30, 39], [-30, 36]]]}},
|
||||
{"type": "Feature", "properties": {"name": "The equator"},
|
||||
"geometry": {"type": "LineString", "coordinates": [[-50, 0], [10, 0]]}},
|
||||
{"type": "Feature", "properties": {"name": "Ports"},
|
||||
"geometry": {"type": "MultiPoint", "coordinates": [[-9.14, 38.72], [-17.44, 14.69], [-23.51, 14.93]]}}
|
||||
]}"#;
|
||||
|
||||
const QUAKES: [(f64, f64); 5] = [
|
||||
(35.68, 139.69),
|
||||
(-33.45, -70.67),
|
||||
(-6.21, 106.85),
|
||||
(41.01, 28.98),
|
||||
(37.77, -122.42),
|
||||
];
|
||||
|
||||
const CAPITALS: [(f64, f64); 14] = [
|
||||
(51.51, -0.13),
|
||||
(48.86, 2.35),
|
||||
(52.52, 13.4),
|
||||
(40.42, -3.7),
|
||||
(41.9, 12.5),
|
||||
(48.21, 16.37),
|
||||
(52.37, 4.9),
|
||||
(50.85, 4.35),
|
||||
(38.72, -9.14),
|
||||
(50.08, 14.44),
|
||||
(52.23, 21.01),
|
||||
(59.33, 18.07),
|
||||
(55.68, 12.57),
|
||||
(47.5, 19.04),
|
||||
];
|
||||
|
||||
fn map(id: &'static str, center: (f64, f64), zoom: f64) -> MapView {
|
||||
MapView::new(id, MapViewport::new(LatLon::new(center.0, center.1), zoom))
|
||||
.tiles(tile)
|
||||
.tile_zooms(0, 3)
|
||||
.attribution(ATTRIBUTION)
|
||||
}
|
||||
|
||||
pub fn routes(cx: &App) -> impl IntoElement + use<> {
|
||||
let features = read_geojson(SHAPES).expect("the demo's GeoJSON reads");
|
||||
let route = RouteLine::new(ROUTE.map(|(lat, lon)| LatLon::new(lat, lon))).hue(0);
|
||||
section(
|
||||
"RouteLine · GeoJSON Layer",
|
||||
"A route along places in order, the short way across the date line. GeoJSON reads to features: areas washed and outlined with their holes open, lines stroked, points dotted. Both repeat on each world in view.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(720.0)).h(px(360.0)).child(
|
||||
map("routes-map", (38.0, -40.0), 2.3)
|
||||
.layer(GeoJsonLayer::new(features).hue(3))
|
||||
.layer(route),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn heat(cx: &App) -> impl IntoElement + use<> {
|
||||
let mut next = noise(41);
|
||||
let points: Vec<(LatLon, f32)> = QUAKES
|
||||
.iter()
|
||||
.flat_map(|(lat, lon)| (0..30).map(move |_| (*lat, *lon)))
|
||||
.map(|(lat, lon)| {
|
||||
let place = LatLon::new(lat + (next() - 0.5) * 6.0, lon + (next() - 0.5) * 6.0);
|
||||
(place, 0.5 + next() as f32 * 1.5)
|
||||
})
|
||||
.collect();
|
||||
section(
|
||||
"GeoHeatmap",
|
||||
"Weighted places as heat, hotter where they crowd, scaled to the densest spot in view.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(720.0))
|
||||
.h(px(360.0))
|
||||
.child(map("heat-map", (20.0, 30.0), 1.0).layer(GeoHeatmap::new(points).hue(6))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clusters(cx: &App) -> impl IntoElement + use<> {
|
||||
let mut next = noise(7);
|
||||
let pins: Vec<MapMarker> = CAPITALS
|
||||
.iter()
|
||||
.flat_map(|(lat, lon)| (0..3).map(move |_| (*lat, *lon)))
|
||||
.enumerate()
|
||||
.map(|(ix, (lat, lon))| {
|
||||
let place = LatLon::new(lat + (next() - 0.5) * 1.2, lon + (next() - 0.5) * 1.8);
|
||||
MapMarker::new(format!("pin-{ix}"), place).hue(ix % 8)
|
||||
})
|
||||
.collect();
|
||||
section(
|
||||
"MapCluster",
|
||||
"Pins that crowd one square of the world gather into a count; a pan keeps each group, a press shows its members whole.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"cluster-map",
|
||||
div().w(px(720.0)).h(px(360.0)).child(
|
||||
map("cluster-map", (42.0, 5.0), 3.0)
|
||||
.markers(pins)
|
||||
.cluster_markers()
|
||||
.on_marker(|key, _, _| log::info!("maps demo: pin {key}")),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -11,6 +11,8 @@ use gpui::{
|
||||
AnyElement, App, ImageSource, IntoElement, ParentElement, SharedString, Styled, Window, div, px,
|
||||
};
|
||||
|
||||
mod layers;
|
||||
|
||||
use super::Page;
|
||||
use crate::probe::probe;
|
||||
use crate::script::Step;
|
||||
@@ -20,7 +22,7 @@ pub const PAGE: Page = Page {
|
||||
number: 41,
|
||||
slug: "maps",
|
||||
title: "Maps",
|
||||
summary: "Tiles the host supplies, pins with a popup, a picker's crosshair. The land is Natural Earth's.",
|
||||
summary: "Tiles the host supplies, pins with a popup, a picker's crosshair, routes, GeoJSON, heat and clusters. The land is Natural Earth's.",
|
||||
render,
|
||||
script: &[
|
||||
Step::Click("world-map"),
|
||||
@@ -37,11 +39,16 @@ pub const PAGE: Page = Page {
|
||||
Step::Key("down"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("picked"),
|
||||
Step::Click("cluster-map"),
|
||||
Step::Key("tab"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("cluster-opened"),
|
||||
],
|
||||
};
|
||||
|
||||
const TILES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/gallery/assets/tiles");
|
||||
const ATTRIBUTION: &str = "Land: Natural Earth";
|
||||
pub(super) const ATTRIBUTION: &str = "Land: Natural Earth";
|
||||
|
||||
const CITIES: [(&str, &str, f64, f64, usize); 6] = [
|
||||
("london", "London", 51.5072, -0.1276, 0),
|
||||
@@ -53,7 +60,7 @@ const CITIES: [(&str, &str, f64, f64, usize); 6] = [
|
||||
];
|
||||
|
||||
/// The gallery's tiles, cut from Natural Earth by `scripts/tiles.py`.
|
||||
fn tile(tile: Tile) -> ImageSource {
|
||||
pub(super) fn tile(tile: Tile) -> ImageSource {
|
||||
PathBuf::from(format!("{TILES}/{}/{}/{}.png", tile.z, tile.x, tile.y)).into()
|
||||
}
|
||||
|
||||
@@ -62,6 +69,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(world(window, cx))
|
||||
.child(coordinates(cx))
|
||||
.child(picker(cx))
|
||||
.child(layers::routes(cx))
|
||||
.child(layers::heat(cx))
|
||||
.child(layers::clusters(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user