oxigdal_algorithms/vector/
area.rs1use 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
42const WGS84_A: f64 = 6_378_137.0;
44
45const WGS84_B: f64 = 6_356_752.314_245;
47
48const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum AreaMethod {
54 Planar,
56 Geodetic,
59 SignedPlanar,
61 KarneyGeodesic,
70}
71
72pub 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
96pub 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
126pub 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
153fn area_polygon_planar(polygon: &Polygon) -> f64 {
157 let mut area = ring_area_planar(&polygon.exterior.coords).abs();
158
159 for hole in &polygon.interiors {
161 area -= ring_area_planar(&hole.coords).abs();
162 }
163
164 area
165}
166
167fn area_polygon_signed(polygon: &Polygon) -> f64 {
171 let mut area = ring_area_planar(&polygon.exterior.coords);
172
173 for hole in &polygon.interiors {
175 area -= ring_area_planar(&hole.coords);
176 }
177
178 area
179}
180
181fn 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
199fn area_polygon_geodetic(polygon: &Polygon) -> Result<f64> {
204 let mut area = ring_area_geodetic(&polygon.exterior.coords)?;
205
206 for hole in &polygon.interiors {
208 area -= ring_area_geodetic(&hole.coords)?;
209 }
210
211 Ok(area.abs())
212}
213
214fn 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 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 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 let dlon = lon2 - lon1;
247
248 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 area += dlon * (lat1.sin() + lat2.sin());
259 }
260
261 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
269pub fn is_counter_clockwise(polygon: &Polygon) -> bool {
279 ring_area_planar(&polygon.exterior.coords) > 0.0
280}
281
282pub 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 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 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 assert!((area - 34.0).abs() < 1e-10);
397 }
398 }
399 }
400
401 #[test]
402 fn test_area_geodetic_small_polygon() {
403 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 assert!(area > 1e10); assert!(area < 2e10); }
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 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), 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}