maps-engine-rust 0.1.0

Zero-dependency map engine core in Rust — Web Mercator projection, tiles, geofencing, clustering, navigation math.
Documentation
//! Grid-based marker clustering. Port of `cluster.ts`.

use crate::geo::LonLat;
use crate::projection::to_pixels;
use std::collections::HashMap;

/// A single cluster of markers.
#[derive(Debug, Clone)]
pub struct Cluster {
    /// Cluster centroid.
    pub center: LonLat,
    /// Number of markers in the cluster.
    pub count: usize,
    /// Indices of the clustered markers in the input slice.
    pub members: Vec<usize>,
}

/// Cluster markers with a grid of `cell_px` pixels at `zoom`.
///
/// Markers falling into the same grid cell are merged. Single-marker
/// cells are returned as clusters with `count == 1`.
pub fn cluster(markers: &[LonLat], zoom: u8, cell_px: f64) -> Vec<Cluster> {
    let mut cells: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
    for (i, m) in markers.iter().enumerate() {
        let (px, py) = to_pixels(m.lon, m.lat, zoom);
        let key = ((px / cell_px).floor() as i64, (py / cell_px).floor() as i64);
        cells.entry(key).or_default().push(i);
    }
    cells
        .into_values()
        .map(|members| {
            let count = members.len();
            let (slon, slat) = members.iter().fold((0.0, 0.0), |(a, b), &i| {
                (a + markers[i].lon, b + markers[i].lat)
            });
            Cluster {
                center: LonLat::new(slon / count as f64, slat / count as f64),
                count,
                members,
            }
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn distant_markers_stay_separate() {
        let ms = vec![LonLat::new(105.0, 21.0), LonLat::new(106.0, 22.0)];
        let c = cluster(&ms, 10, 64.0);
        assert_eq!(c.len(), 2);
        assert!(c.iter().all(|x| x.count == 1));
    }

    #[test]
    fn nearby_markers_merge() {
        let ms = vec![
            LonLat::new(105.8500, 21.0200),
            LonLat::new(105.8501, 21.0201),
            LonLat::new(105.8502, 21.0199),
        ];
        let c = cluster(&ms, 14, 64.0);
        assert_eq!(c.len(), 1);
        assert_eq!(c[0].count, 3);
        assert_eq!(c[0].members.len(), 3);
    }

    #[test]
    fn members_cover_all_inputs() {
        let ms: Vec<LonLat> = (0..50)
            .map(|i| LonLat::new(105.0 + i as f64 * 0.01, 21.0))
            .collect();
        let c = cluster(&ms, 10, 64.0);
        let total: usize = c.iter().map(|x| x.count).sum();
        assert_eq!(total, 50);
    }
}