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:
1 parent
f5909f6e43
commit
a3115b8126
16 files changed
+1350
-48
No files matched your search
@@ -0,0 +1,119 @@
|
||||
/// Density over a view in square cells, the densest cell 1.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Heat {
|
||||
pub columns: usize,
|
||||
pub rows: usize,
|
||||
pub cell: f32,
|
||||
pub values: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Heat {
|
||||
pub fn at(&self, column: usize, row: usize) -> f32 {
|
||||
self.values[row * self.columns + column]
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(crate) fn density(
|
||||
points: &[((f32, f32), f32)],
|
||||
(w, h): (f32, f32),
|
||||
cell: f32,
|
||||
reach: f32,
|
||||
) -> Heat {
|
||||
assert!(
|
||||
cell > 0.0 && reach > 0.0,
|
||||
"heat cells of {cell} reaching {reach}"
|
||||
);
|
||||
let (columns, rows) = (
|
||||
(w / cell).ceil().max(0.0) as usize,
|
||||
(h / cell).ceil().max(0.0) as usize,
|
||||
);
|
||||
let mut values = vec![0.0f32; columns * rows];
|
||||
for &((x, y), weight) in points {
|
||||
assert!(
|
||||
weight.is_finite() && weight >= 0.0,
|
||||
"a heat weight of {weight}"
|
||||
);
|
||||
let span = |at: f32, count: usize| {
|
||||
let first = ((at - reach) / cell).floor().max(0.0) as usize;
|
||||
let last = (((at + reach) / cell).ceil().max(0.0) as usize).min(count);
|
||||
first..last
|
||||
};
|
||||
for row in span(y, rows) {
|
||||
for column in span(x, columns) {
|
||||
let (cx, cy) = ((column as f32 + 0.5) * cell, (row as f32 + 0.5) * cell);
|
||||
let near = ((cx - x).powi(2) + (cy - y).powi(2)) / reach.powi(2);
|
||||
if near < 1.0 {
|
||||
values[row * columns + column] += weight * (1.0 - near).powi(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let most = values.iter().copied().fold(0.0f32, f32::max);
|
||||
if most > 0.0 {
|
||||
values.iter_mut().for_each(|value| *value /= most);
|
||||
}
|
||||
Heat {
|
||||
columns,
|
||||
rows,
|
||||
cell,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::density;
|
||||
|
||||
#[test]
|
||||
fn heat_peaks_at_its_point_and_ends_at_its_reach() {
|
||||
let heat = density(&[((25.0, 25.0), 2.0)], (100.0, 60.0), 10.0, 20.0);
|
||||
assert_eq!((heat.columns, heat.rows), (10, 6));
|
||||
assert_eq!(
|
||||
heat.at(2, 2),
|
||||
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(6, 2), 0.0, "past its reach nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heavier_points_run_hotter_and_heat_adds_up() {
|
||||
let heat = density(
|
||||
&[((15.0, 15.0), 1.0), ((85.0, 15.0), 3.0)],
|
||||
(100.0, 30.0),
|
||||
10.0,
|
||||
15.0,
|
||||
);
|
||||
assert!(
|
||||
heat.at(8, 1) > heat.at(1, 1) * 2.5,
|
||||
"three times the weight"
|
||||
);
|
||||
let pair = density(
|
||||
&[((45.0, 15.0), 1.0), ((55.0, 15.0), 1.0)],
|
||||
(100.0, 30.0),
|
||||
10.0,
|
||||
15.0,
|
||||
);
|
||||
assert_eq!(
|
||||
pair.at(4, 1),
|
||||
pair.at(5, 1),
|
||||
"two alike points meet in the middle"
|
||||
);
|
||||
let none = density(&[], (100.0, 30.0), 10.0, 15.0);
|
||||
assert!(
|
||||
none.values.iter().all(|value| *value == 0.0),
|
||||
"no points, no heat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "a heat weight of -1")]
|
||||
fn a_negative_weight_fails() {
|
||||
density(&[((0.0, 0.0), -1.0)], (10.0, 10.0), 5.0, 5.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user