Skip to main content

maps_engine_rust/
measure.rs

1//! Distance / area measurement. Port of `measure.ts` + `measure-live.ts`.
2
3use crate::geo::{haversine, LonLat};
4
5/// Total distance of a polyline in meters.
6pub fn measure_distance(points: &[LonLat]) -> f64 {
7    points.windows(2).map(|w| haversine(w[0], w[1])).sum()
8}
9
10/// Approximate area of a polygon in square meters, using an
11/// equirectangular projection around the polygon centroid.
12pub fn measure_area(vertices: &[LonLat]) -> f64 {
13    if vertices.len() < 3 {
14        return 0.0;
15    }
16    let n = vertices.len() as f64;
17    let clon = vertices.iter().map(|p| p.lon).sum::<f64>() / n;
18    let clat = vertices.iter().map(|p| p.lat).sum::<f64>() / n;
19    let kx = 111_320.0 * (clat.to_radians().cos());
20    let ky = 110_540.0;
21    let pts: Vec<(f64, f64)> = vertices
22        .iter()
23        .map(|p| ((p.lon - clon) * kx, (p.lat - clat) * ky))
24        .collect();
25    // Shoelace formula.
26    let mut area = 0.0;
27    for i in 0..pts.len() {
28        let (x1, y1) = pts[i];
29        let (x2, y2) = pts[(i + 1) % pts.len()];
30        area += x1 * y2 - x2 * y1;
31    }
32    area.abs() / 2.0
33}
34
35/// Human-readable distance, e.g. "850 m" or "12.4 km".
36pub fn format_distance(meters: f64) -> String {
37    if meters < 1000.0 {
38        format!("{} m", meters.round() as i64)
39    } else {
40        format!("{:.1} km", meters / 1000.0)
41    }
42}
43
44/// Human-readable area, e.g. "450 m²" or "2.3 km²".
45pub fn format_area(sq_meters: f64) -> String {
46    if sq_meters < 1_000_000.0 {
47        format!("{} m²", sq_meters.round() as i64)
48    } else {
49        format!("{:.2} km²", sq_meters / 1_000_000.0)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn distance_hanoi_haiphong() {
59        let d = measure_distance(&[LonLat::new(105.85, 21.02), LonLat::new(106.68, 20.86)]);
60        assert!((d - 88_000.0).abs() < 5_000.0);
61    }
62
63    #[test]
64    fn area_unit_square_degrees() {
65        // 1°×1° square at equator ≈ (111.32km)²
66        let sq = vec![
67            LonLat::new(0.0, 0.0),
68            LonLat::new(1.0, 0.0),
69            LonLat::new(1.0, 1.0),
70            LonLat::new(0.0, 1.0),
71        ];
72        let a = measure_area(&sq);
73        let expected = 111_320.0 * 110_540.0;
74        assert!((a - expected).abs() / expected < 0.02, "got {a}");
75    }
76
77    #[test]
78    fn degenerate_area_is_zero() {
79        assert_eq!(measure_area(&[]), 0.0);
80        assert_eq!(measure_area(&[LonLat::new(0.0, 0.0)]), 0.0);
81    }
82
83    #[test]
84    fn formatting() {
85        assert_eq!(format_distance(850.4), "850 m");
86        assert_eq!(format_distance(12_400.0), "12.4 km");
87        assert_eq!(format_area(450.0), "450 m²");
88        assert_eq!(format_area(2_300_000.0), "2.30 km²");
89    }
90}