Skip to main content

oxigdal_algorithms/vector/
area.rs

1//! Area calculation for polygonal geometries
2//!
3//! This module provides functions for computing areas of polygons using
4//! different methods appropriate for planar and geodetic coordinates.
5//!
6//! # Area Methods
7//!
8//! - **Planar**: Fast area calculation assuming Cartesian coordinates (projected data)
9//! - **Geodetic**: Accurate area on Earth's surface using WGS84 ellipsoid
10//! - **Signed**: Preserves orientation information (counter-clockwise = positive)
11//!
12//! # Examples
13//!
14//! ```
15//! use oxigdal_algorithms::vector::{Coordinate, Polygon, LineString, area_polygon, AreaMethod};
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let coords = vec![
19//!     Coordinate::new_2d(0.0, 0.0),
20//!     Coordinate::new_2d(10.0, 0.0),
21//!     Coordinate::new_2d(10.0, 10.0),
22//!     Coordinate::new_2d(0.0, 10.0),
23//!     Coordinate::new_2d(0.0, 0.0),
24//! ];
25//! let exterior = LineString::new(coords)?;
26//! let polygon = Polygon::new(exterior, vec![])?;
27//! let area_planar = area_polygon(&polygon, AreaMethod::Planar)?;
28//! // Planar area = 100.0 square units
29//! # Ok(())
30//! # }
31//! ```
32
33use crate::error::{AlgorithmError, Result};
34use oxigdal_core::vector::{Coordinate, Geometry, MultiPolygon, Polygon};
35
36#[cfg(feature = "std")]
37use std::f64::consts::PI;
38
39#[cfg(not(feature = "std"))]
40use core::f64::consts::PI;
41
42/// WGS84 ellipsoid semi-major axis (meters)
43const WGS84_A: f64 = 6_378_137.0;
44
45/// WGS84 ellipsoid semi-minor axis (meters)
46const WGS84_B: f64 = 6_356_752.314_245;
47
48/// WGS84 ellipsoid flattening
49const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
50
51/// Area calculation method
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum AreaMethod {
54    /// Planar area using Cartesian coordinates (fast, for projected data)
55    Planar,
56    /// Geodetic area on WGS84 ellipsoid using spherical trapezoid rule
57    /// (good approximation for lat/lon coordinates)
58    Geodetic,
59    /// Signed planar area (preserves orientation)
60    SignedPlanar,
61    /// Karney's geodesic area on WGS84 ellipsoid (highest accuracy)
62    ///
63    /// Uses 6th-order Taylor expansion in the third flattening with the
64    /// full inverse geodesic solver. Sub-millimeter accuracy for any polygon
65    /// size including antimeridian-crossing and polar-enclosing geometries.
66    ///
67    /// Reference: Karney, C.F.F. (2013) "Algorithms for geodesics",
68    /// Journal of Geodesy 87(1), pp. 43-55.
69    KarneyGeodesic,
70}
71
72/// Computes the area of a geometry
73///
74/// # Arguments
75///
76/// * `geometry` - Input geometry
77/// * `method` - Area calculation method
78///
79/// # Returns
80///
81/// Area in appropriate units (square meters for geodetic, coordinate units for planar)
82///
83/// # Errors
84///
85/// Returns error if geometry is not polygonal or invalid
86pub fn area(geometry: &Geometry, method: AreaMethod) -> Result<f64> {
87    match geometry {
88        Geometry::Polygon(p) => area_polygon(p, method),
89        Geometry::MultiPolygon(mp) => area_multipolygon(mp, method),
90        _ => Err(AlgorithmError::GeometryError {
91            message: "area calculation requires Polygon or MultiPolygon geometry".to_string(),
92        }),
93    }
94}
95
96/// Computes the area of a polygon
97///
98/// # Arguments
99///
100/// * `polygon` - Input polygon
101/// * `method` - Area calculation method
102///
103/// # Returns
104///
105/// Area value (always non-negative unless using SignedPlanar method)
106///
107/// # Errors
108///
109/// Returns error if polygon is invalid
110pub fn area_polygon(polygon: &Polygon, method: AreaMethod) -> Result<f64> {
111    if polygon.exterior.coords.len() < 3 {
112        return Err(AlgorithmError::InsufficientData {
113            operation: "area_polygon",
114            message: "polygon must have at least 3 coordinates".to_string(),
115        });
116    }
117
118    match method {
119        AreaMethod::Planar => Ok(area_polygon_planar(polygon)),
120        AreaMethod::Geodetic => area_polygon_geodetic(polygon),
121        AreaMethod::SignedPlanar => Ok(area_polygon_signed(polygon)),
122        AreaMethod::KarneyGeodesic => super::geodesic::area_polygon_karney(polygon),
123    }
124}
125
126/// Computes the area of a multipolygon
127///
128/// # Arguments
129///
130/// * `multipolygon` - Input multipolygon
131/// * `method` - Area calculation method
132///
133/// # Returns
134///
135/// Total area of all polygons
136///
137/// # Errors
138///
139/// Returns error if any polygon is invalid
140pub fn area_multipolygon(multipolygon: &MultiPolygon, method: AreaMethod) -> Result<f64> {
141    if multipolygon.polygons.is_empty() {
142        return Ok(0.0);
143    }
144
145    let mut total = 0.0;
146    for polygon in &multipolygon.polygons {
147        total += area_polygon(polygon, method)?;
148    }
149
150    Ok(total)
151}
152
153/// Computes planar area of a polygon (fast, Cartesian)
154///
155/// Uses the shoelace formula (Gauss's area formula).
156fn area_polygon_planar(polygon: &Polygon) -> f64 {
157    let mut area = ring_area_planar(&polygon.exterior.coords).abs();
158
159    // Subtract holes
160    for hole in &polygon.interiors {
161        area -= ring_area_planar(&hole.coords).abs();
162    }
163
164    area
165}
166
167/// Computes signed planar area of a polygon
168///
169/// Positive for counter-clockwise orientation, negative for clockwise.
170fn area_polygon_signed(polygon: &Polygon) -> f64 {
171    let mut area = ring_area_planar(&polygon.exterior.coords);
172
173    // Subtract holes (with their signs)
174    for hole in &polygon.interiors {
175        area -= ring_area_planar(&hole.coords);
176    }
177
178    area
179}
180
181/// Computes planar area of a ring using shoelace formula
182fn ring_area_planar(coords: &[Coordinate]) -> f64 {
183    if coords.len() < 3 {
184        return 0.0;
185    }
186
187    let mut area = 0.0;
188    let n = coords.len();
189
190    for i in 0..n {
191        let j = (i + 1) % n;
192        area += coords[i].x * coords[j].y;
193        area -= coords[j].x * coords[i].y;
194    }
195
196    area / 2.0
197}
198
199/// Computes geodetic area of a polygon on WGS84 ellipsoid
200///
201/// Uses the method from Bessel (1825) for geodetic polygons.
202/// Coordinates must be in longitude/latitude (degrees).
203fn area_polygon_geodetic(polygon: &Polygon) -> Result<f64> {
204    let mut area = ring_area_geodetic(&polygon.exterior.coords)?;
205
206    // Subtract holes
207    for hole in &polygon.interiors {
208        area -= ring_area_geodetic(&hole.coords)?;
209    }
210
211    Ok(area.abs())
212}
213
214/// Computes geodetic area of a ring
215///
216/// Implementation based on:
217/// Chamberlain, R. G., and W. H. Duquette. "Some algorithms for polygons on a sphere."
218/// JPL Publication 07-03 (2007).
219fn ring_area_geodetic(coords: &[Coordinate]) -> Result<f64> {
220    if coords.len() < 3 {
221        return Ok(0.0);
222    }
223
224    let n = coords.len();
225    let mut area = 0.0;
226
227    for i in 0..n - 1 {
228        let p1 = &coords[i];
229        let p2 = &coords[i + 1];
230
231        // Convert to radians
232        let lat1 = p1.y * PI / 180.0;
233        let lon1 = p1.x * PI / 180.0;
234        let lat2 = p2.y * PI / 180.0;
235        let lon2 = p2.x * PI / 180.0;
236
237        // Validate latitude range
238        if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
239            return Err(AlgorithmError::InvalidParameter {
240                parameter: "latitude",
241                message: "latitude must be between -90 and 90 degrees".to_string(),
242            });
243        }
244
245        // Calculate area contribution of this edge
246        let dlon = lon2 - lon1;
247
248        // Handle antimeridian crossing
249        let dlon = if dlon > PI {
250            dlon - 2.0 * PI
251        } else if dlon < -PI {
252            dlon + 2.0 * PI
253        } else {
254            dlon
255        };
256
257        // Accumulate area using trapezoid rule on the sphere
258        area += dlon * (lat1.sin() + lat2.sin());
259    }
260
261    // Convert to square meters using WGS84 parameters
262    let authalic_radius =
263        ((WGS84_A * WGS84_A + WGS84_B * WGS84_B / (1.0 - WGS84_F).sqrt()) / 2.0).sqrt();
264    let area_m2 = area.abs() * authalic_radius * authalic_radius / 2.0;
265
266    Ok(area_m2)
267}
268
269/// Checks if a polygon is oriented counter-clockwise
270///
271/// # Arguments
272///
273/// * `polygon` - Input polygon
274///
275/// # Returns
276///
277/// True if exterior ring is counter-clockwise, false otherwise
278pub fn is_counter_clockwise(polygon: &Polygon) -> bool {
279    ring_area_planar(&polygon.exterior.coords) > 0.0
280}
281
282/// Checks if a polygon is oriented clockwise
283///
284/// # Arguments
285///
286/// * `polygon` - Input polygon
287///
288/// # Returns
289///
290/// True if exterior ring is clockwise, false otherwise
291pub fn is_clockwise(polygon: &Polygon) -> bool {
292    ring_area_planar(&polygon.exterior.coords) < 0.0
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use oxigdal_core::vector::LineString;
299
300    fn create_square(size: f64) -> Result<Polygon> {
301        let coords = vec![
302            Coordinate::new_2d(0.0, 0.0),
303            Coordinate::new_2d(size, 0.0),
304            Coordinate::new_2d(size, size),
305            Coordinate::new_2d(0.0, size),
306            Coordinate::new_2d(0.0, 0.0),
307        ];
308        let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
309        Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
310    }
311
312    fn create_square_with_hole() -> Result<Polygon> {
313        let exterior_coords = vec![
314            Coordinate::new_2d(0.0, 0.0),
315            Coordinate::new_2d(10.0, 0.0),
316            Coordinate::new_2d(10.0, 10.0),
317            Coordinate::new_2d(0.0, 10.0),
318            Coordinate::new_2d(0.0, 0.0),
319        ];
320        let hole_coords = vec![
321            Coordinate::new_2d(2.0, 2.0),
322            Coordinate::new_2d(8.0, 2.0),
323            Coordinate::new_2d(8.0, 8.0),
324            Coordinate::new_2d(2.0, 8.0),
325            Coordinate::new_2d(2.0, 2.0),
326        ];
327
328        let exterior = LineString::new(exterior_coords).map_err(|e| AlgorithmError::Core(e))?;
329        let hole = LineString::new(hole_coords).map_err(|e| AlgorithmError::Core(e))?;
330
331        Polygon::new(exterior, vec![hole]).map_err(|e| AlgorithmError::Core(e))
332    }
333
334    #[test]
335    fn test_area_polygon_planar() {
336        let poly = create_square(10.0);
337        assert!(poly.is_ok());
338
339        if let Ok(p) = poly {
340            let result = area_polygon(&p, AreaMethod::Planar);
341            assert!(result.is_ok());
342
343            if let Ok(area) = result {
344                assert!((area - 100.0).abs() < 1e-10);
345            }
346        }
347    }
348
349    #[test]
350    fn test_area_polygon_with_hole() {
351        let poly = create_square_with_hole();
352        assert!(poly.is_ok());
353
354        if let Ok(p) = poly {
355            let result = area_polygon(&p, AreaMethod::Planar);
356            assert!(result.is_ok());
357
358            if let Ok(area) = result {
359                // Outer area = 100, hole area = 36, effective = 64
360                assert!((area - 64.0).abs() < 1e-10);
361            }
362        }
363    }
364
365    #[test]
366    fn test_area_polygon_signed() {
367        let poly = create_square(10.0);
368        assert!(poly.is_ok());
369
370        if let Ok(p) = poly {
371            let result = area_polygon(&p, AreaMethod::SignedPlanar);
372            assert!(result.is_ok());
373
374            if let Ok(area) = result {
375                // CCW orientation = positive area
376                assert!(area > 0.0);
377                assert!((area.abs() - 100.0).abs() < 1e-10);
378            }
379        }
380    }
381
382    #[test]
383    fn test_area_multipolygon() {
384        let poly1 = create_square(5.0);
385        let poly2 = create_square(3.0);
386
387        assert!(poly1.is_ok() && poly2.is_ok());
388
389        if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
390            let mp = MultiPolygon::new(vec![p1, p2]);
391            let result = area_multipolygon(&mp, AreaMethod::Planar);
392            assert!(result.is_ok());
393
394            if let Ok(area) = result {
395                // 5*5 + 3*3 = 25 + 9 = 34
396                assert!((area - 34.0).abs() < 1e-10);
397            }
398        }
399    }
400
401    #[test]
402    fn test_area_geodetic_small_polygon() {
403        // Small polygon in degrees (approximately 1 degree square near equator)
404        let coords = vec![
405            Coordinate::new_2d(0.0, 0.0),
406            Coordinate::new_2d(1.0, 0.0),
407            Coordinate::new_2d(1.0, 1.0),
408            Coordinate::new_2d(0.0, 1.0),
409            Coordinate::new_2d(0.0, 0.0),
410        ];
411        let exterior = LineString::new(coords);
412        assert!(exterior.is_ok());
413
414        if let Ok(ext) = exterior {
415            let poly = Polygon::new(ext, vec![]);
416            assert!(poly.is_ok());
417
418            if let Ok(p) = poly {
419                let result = area_polygon(&p, AreaMethod::Geodetic);
420                assert!(result.is_ok());
421
422                if let Ok(area) = result {
423                    // 1 degree at equator is approximately 111 km
424                    // So 1 degree square is approximately 111 * 111 = 12,321 km^2
425                    // = 12,321,000,000 m^2
426                    assert!(area > 1e10); // Should be in billions of square meters
427                    assert!(area < 2e10); // Rough upper bound
428                }
429            }
430        }
431    }
432
433    #[test]
434    fn test_is_counter_clockwise() {
435        let poly = create_square(10.0);
436        assert!(poly.is_ok());
437
438        if let Ok(p) = poly {
439            assert!(is_counter_clockwise(&p));
440            assert!(!is_clockwise(&p));
441        }
442    }
443
444    #[test]
445    fn test_ring_area_planar() {
446        let coords = vec![
447            Coordinate::new_2d(0.0, 0.0),
448            Coordinate::new_2d(10.0, 0.0),
449            Coordinate::new_2d(10.0, 10.0),
450            Coordinate::new_2d(0.0, 10.0),
451            Coordinate::new_2d(0.0, 0.0),
452        ];
453
454        let area = ring_area_planar(&coords);
455        assert!((area.abs() - 100.0).abs() < 1e-10);
456    }
457
458    #[test]
459    fn test_area_empty_multipolygon() {
460        let mp = MultiPolygon::empty();
461        let result = area_multipolygon(&mp, AreaMethod::Planar);
462        assert!(result.is_ok());
463
464        if let Ok(area) = result {
465            assert_eq!(area, 0.0);
466        }
467    }
468
469    #[test]
470    fn test_area_invalid_geometry() {
471        let point = Geometry::Point(oxigdal_core::vector::Point::new(0.0, 0.0));
472        let result = area(&point, AreaMethod::Planar);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_geodetic_area_invalid_latitude() {
478        // Latitude > 90 degrees
479        let coords = vec![
480            Coordinate::new_2d(0.0, 0.0),
481            Coordinate::new_2d(1.0, 0.0),
482            Coordinate::new_2d(1.0, 100.0), // Invalid
483            Coordinate::new_2d(0.0, 1.0),
484            Coordinate::new_2d(0.0, 0.0),
485        ];
486        let exterior = LineString::new(coords);
487        assert!(exterior.is_ok());
488
489        if let Ok(ext) = exterior {
490            let poly = Polygon::new(ext, vec![]);
491            assert!(poly.is_ok());
492
493            if let Ok(p) = poly {
494                let result = area_polygon(&p, AreaMethod::Geodetic);
495                assert!(result.is_err());
496            }
497        }
498    }
499}