maps-engine-rust 0.1.0

Zero-dependency map engine core in Rust — Web Mercator projection, tiles, geofencing, clustering, navigation math.
Documentation
//! Distance / area measurement. Port of `measure.ts` + `measure-live.ts`.

use crate::geo::{haversine, LonLat};

/// Total distance of a polyline in meters.
pub fn measure_distance(points: &[LonLat]) -> f64 {
    points.windows(2).map(|w| haversine(w[0], w[1])).sum()
}

/// Approximate area of a polygon in square meters, using an
/// equirectangular projection around the polygon centroid.
pub fn measure_area(vertices: &[LonLat]) -> f64 {
    if vertices.len() < 3 {
        return 0.0;
    }
    let n = vertices.len() as f64;
    let clon = vertices.iter().map(|p| p.lon).sum::<f64>() / n;
    let clat = vertices.iter().map(|p| p.lat).sum::<f64>() / n;
    let kx = 111_320.0 * (clat.to_radians().cos());
    let ky = 110_540.0;
    let pts: Vec<(f64, f64)> = vertices
        .iter()
        .map(|p| ((p.lon - clon) * kx, (p.lat - clat) * ky))
        .collect();
    // Shoelace formula.
    let mut area = 0.0;
    for i in 0..pts.len() {
        let (x1, y1) = pts[i];
        let (x2, y2) = pts[(i + 1) % pts.len()];
        area += x1 * y2 - x2 * y1;
    }
    area.abs() / 2.0
}

/// Human-readable distance, e.g. "850 m" or "12.4 km".
pub fn format_distance(meters: f64) -> String {
    if meters < 1000.0 {
        format!("{} m", meters.round() as i64)
    } else {
        format!("{:.1} km", meters / 1000.0)
    }
}

/// Human-readable area, e.g. "450 m²" or "2.3 km²".
pub fn format_area(sq_meters: f64) -> String {
    if sq_meters < 1_000_000.0 {
        format!("{} m²", sq_meters.round() as i64)
    } else {
        format!("{:.2} km²", sq_meters / 1_000_000.0)
    }
}

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

    #[test]
    fn distance_hanoi_haiphong() {
        let d = measure_distance(&[LonLat::new(105.85, 21.02), LonLat::new(106.68, 20.86)]);
        assert!((d - 88_000.0).abs() < 5_000.0);
    }

    #[test]
    fn area_unit_square_degrees() {
        // 1°×1° square at equator ≈ (111.32km)²
        let sq = vec![
            LonLat::new(0.0, 0.0),
            LonLat::new(1.0, 0.0),
            LonLat::new(1.0, 1.0),
            LonLat::new(0.0, 1.0),
        ];
        let a = measure_area(&sq);
        let expected = 111_320.0 * 110_540.0;
        assert!((a - expected).abs() / expected < 0.02, "got {a}");
    }

    #[test]
    fn degenerate_area_is_zero() {
        assert_eq!(measure_area(&[]), 0.0);
        assert_eq!(measure_area(&[LonLat::new(0.0, 0.0)]), 0.0);
    }

    #[test]
    fn formatting() {
        assert_eq!(format_distance(850.4), "850 m");
        assert_eq!(format_distance(12_400.0), "12.4 km");
        assert_eq!(format_area(450.0), "450 m²");
        assert_eq!(format_area(2_300_000.0), "2.30 km²");
    }
}