Skip to main content

geonative_utils/
measure.rs

1//! Linear and areal measurements on the geonative-core IR.
2//!
3//! ## What this module is
4//!
5//! Pure-math primitives: Euclidean distance between two coords, length of
6//! a polyline, signed/unsigned area of a polygon ring. They operate in the
7//! same units as their input (degrees for lng/lat data, meters for projected
8//! data) — there is **no** geodetic correction here. If you need
9//! great-circle distances, project to a metric CRS first or wait for a
10//! future `geonative-proj`-aware measure module.
11//!
12//! ## Clever bits
13//!
14//! - **`signed_area` uses the shoelace formula directly.** Positive result
15//!   means CCW ring (OGC exterior); negative means CW (OGC interior).
16//!   Callers that just want magnitude take `.abs()`.
17//! - **`area` of a `Polygon` subtracts hole areas from the exterior.**
18//!   Standard "shell minus holes" formula. Works for any polygon as long
19//!   as holes are inside the exterior (the IR's invariant).
20//! - **`length` ignores empty/single-point inputs** (returns 0.0).
21//! - **No allocation.** All four entry points are non-allocating.
22
23use geonative_core::{Coord, LineString, Polygon};
24
25/// Euclidean distance between two coords in the input units (degrees /
26/// meters / whatever).
27pub fn distance(a: Coord, b: Coord) -> f64 {
28    let dx = b.x - a.x;
29    let dy = b.y - a.y;
30    (dx * dx + dy * dy).sqrt()
31}
32
33/// Total length of a polyline (sum of segment lengths). Empty or
34/// single-point inputs return 0.
35pub fn length(ls: &LineString) -> f64 {
36    if ls.coords.len() < 2 {
37        return 0.0;
38    }
39    let mut total = 0.0;
40    for w in ls.coords.windows(2) {
41        total += distance(w[0], w[1]);
42    }
43    total
44}
45
46/// **Signed** area of a ring using the shoelace formula. Positive = CCW
47/// (OGC exterior convention); negative = CW (interior / hole convention).
48/// Rings with < 3 coords return 0.
49pub fn signed_area(ring: &[Coord]) -> f64 {
50    if ring.len() < 3 {
51        return 0.0;
52    }
53    let mut sum = 0.0;
54    let n = ring.len();
55    for i in 0..n {
56        let j = (i + 1) % n;
57        sum += ring[i].x * ring[j].y - ring[j].x * ring[i].y;
58    }
59    sum * 0.5
60}
61
62/// Unsigned area of a single ring.
63pub fn ring_area(ring: &[Coord]) -> f64 {
64    signed_area(ring).abs()
65}
66
67/// Area of a polygon = exterior area minus the sum of hole areas. Returns
68/// the absolute value (sign of the exterior winding doesn't matter for the
69/// final magnitude).
70pub fn area(p: &Polygon) -> f64 {
71    let exterior = ring_area(&p.exterior.coords);
72    let holes: f64 = p.holes.iter().map(|h| ring_area(&h.coords)).sum();
73    (exterior - holes).max(0.0)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn xy(x: f64, y: f64) -> Coord {
81        Coord {
82            x,
83            y,
84            z: None,
85            m: None,
86        }
87    }
88
89    #[test]
90    fn distance_matches_pythagoras() {
91        assert!((distance(xy(0.0, 0.0), xy(3.0, 4.0)) - 5.0).abs() < 1e-12);
92        assert_eq!(distance(xy(1.0, 1.0), xy(1.0, 1.0)), 0.0);
93    }
94
95    #[test]
96    fn length_of_two_segment_line() {
97        let ls = LineString::new(vec![xy(0.0, 0.0), xy(3.0, 0.0), xy(3.0, 4.0)]);
98        assert!((length(&ls) - 7.0).abs() < 1e-12);
99    }
100
101    #[test]
102    fn length_of_short_input_is_zero() {
103        assert_eq!(length(&LineString::default()), 0.0);
104        assert_eq!(length(&LineString::new(vec![xy(1.0, 1.0)])), 0.0);
105    }
106
107    #[test]
108    fn signed_area_unit_square_ccw_is_one() {
109        let ring = vec![
110            xy(0.0, 0.0),
111            xy(1.0, 0.0),
112            xy(1.0, 1.0),
113            xy(0.0, 1.0),
114            xy(0.0, 0.0),
115        ];
116        assert!((signed_area(&ring) - 1.0).abs() < 1e-12);
117    }
118
119    #[test]
120    fn signed_area_unit_square_cw_is_minus_one() {
121        let mut ring = vec![
122            xy(0.0, 0.0),
123            xy(1.0, 0.0),
124            xy(1.0, 1.0),
125            xy(0.0, 1.0),
126            xy(0.0, 0.0),
127        ];
128        ring.reverse();
129        assert!((signed_area(&ring) - (-1.0)).abs() < 1e-12);
130    }
131
132    #[test]
133    fn ring_area_is_unsigned() {
134        let ring = vec![
135            xy(0.0, 0.0),
136            xy(1.0, 0.0),
137            xy(1.0, 1.0),
138            xy(0.0, 1.0),
139            xy(0.0, 0.0),
140        ];
141        assert!((ring_area(&ring) - 1.0).abs() < 1e-12);
142        let mut reversed = ring.clone();
143        reversed.reverse();
144        assert!((ring_area(&reversed) - 1.0).abs() < 1e-12);
145    }
146
147    #[test]
148    fn polygon_area_subtracts_holes() {
149        let p = Polygon::new(
150            LineString::new(vec![
151                xy(0.0, 0.0),
152                xy(10.0, 0.0),
153                xy(10.0, 10.0),
154                xy(0.0, 10.0),
155                xy(0.0, 0.0),
156            ]),
157            vec![LineString::new(vec![
158                xy(2.0, 2.0),
159                xy(4.0, 2.0),
160                xy(4.0, 4.0),
161                xy(2.0, 4.0),
162                xy(2.0, 2.0),
163            ])],
164        );
165        // exterior 100 - hole 4 = 96
166        assert!((area(&p) - 96.0).abs() < 1e-12);
167    }
168
169    #[test]
170    fn polygon_with_multiple_holes_sums_them() {
171        let p = Polygon::new(
172            LineString::new(vec![
173                xy(0.0, 0.0),
174                xy(10.0, 0.0),
175                xy(10.0, 10.0),
176                xy(0.0, 10.0),
177                xy(0.0, 0.0),
178            ]),
179            vec![
180                LineString::new(vec![
181                    xy(1.0, 1.0),
182                    xy(2.0, 1.0),
183                    xy(2.0, 2.0),
184                    xy(1.0, 2.0),
185                    xy(1.0, 1.0),
186                ]),
187                LineString::new(vec![
188                    xy(5.0, 5.0),
189                    xy(8.0, 5.0),
190                    xy(8.0, 8.0),
191                    xy(5.0, 8.0),
192                    xy(5.0, 5.0),
193                ]),
194            ],
195        );
196        // 100 - 1 - 9 = 90
197        assert!((area(&p) - 90.0).abs() < 1e-12);
198    }
199
200    #[test]
201    fn degenerate_ring_zero_area() {
202        assert_eq!(signed_area(&[]), 0.0);
203        assert_eq!(signed_area(&[xy(0.0, 0.0)]), 0.0);
204        assert_eq!(signed_area(&[xy(0.0, 0.0), xy(1.0, 1.0)]), 0.0);
205    }
206}