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,
58 SignedPlanar,
60}
61
62pub fn area(geometry: &Geometry, method: AreaMethod) -> Result<f64> {
77 match geometry {
78 Geometry::Polygon(p) => area_polygon(p, method),
79 Geometry::MultiPolygon(mp) => area_multipolygon(mp, method),
80 _ => Err(AlgorithmError::GeometryError {
81 message: "area calculation requires Polygon or MultiPolygon geometry".to_string(),
82 }),
83 }
84}
85
86pub fn area_polygon(polygon: &Polygon, method: AreaMethod) -> Result<f64> {
101 if polygon.exterior.coords.len() < 3 {
102 return Err(AlgorithmError::InsufficientData {
103 operation: "area_polygon",
104 message: "polygon must have at least 3 coordinates".to_string(),
105 });
106 }
107
108 match method {
109 AreaMethod::Planar => Ok(area_polygon_planar(polygon)),
110 AreaMethod::Geodetic => area_polygon_geodetic(polygon),
111 AreaMethod::SignedPlanar => Ok(area_polygon_signed(polygon)),
112 }
113}
114
115pub fn area_multipolygon(multipolygon: &MultiPolygon, method: AreaMethod) -> Result<f64> {
130 if multipolygon.polygons.is_empty() {
131 return Ok(0.0);
132 }
133
134 let mut total = 0.0;
135 for polygon in &multipolygon.polygons {
136 total += area_polygon(polygon, method)?;
137 }
138
139 Ok(total)
140}
141
142fn area_polygon_planar(polygon: &Polygon) -> f64 {
146 let mut area = ring_area_planar(&polygon.exterior.coords).abs();
147
148 for hole in &polygon.interiors {
150 area -= ring_area_planar(&hole.coords).abs();
151 }
152
153 area
154}
155
156fn area_polygon_signed(polygon: &Polygon) -> f64 {
160 let mut area = ring_area_planar(&polygon.exterior.coords);
161
162 for hole in &polygon.interiors {
164 area -= ring_area_planar(&hole.coords);
165 }
166
167 area
168}
169
170fn ring_area_planar(coords: &[Coordinate]) -> f64 {
172 if coords.len() < 3 {
173 return 0.0;
174 }
175
176 let mut area = 0.0;
177 let n = coords.len();
178
179 for i in 0..n {
180 let j = (i + 1) % n;
181 area += coords[i].x * coords[j].y;
182 area -= coords[j].x * coords[i].y;
183 }
184
185 area / 2.0
186}
187
188fn area_polygon_geodetic(polygon: &Polygon) -> Result<f64> {
193 let mut area = ring_area_geodetic(&polygon.exterior.coords)?;
194
195 for hole in &polygon.interiors {
197 area -= ring_area_geodetic(&hole.coords)?;
198 }
199
200 Ok(area.abs())
201}
202
203fn ring_area_geodetic(coords: &[Coordinate]) -> Result<f64> {
209 if coords.len() < 3 {
210 return Ok(0.0);
211 }
212
213 let n = coords.len();
214 let mut area = 0.0;
215
216 for i in 0..n - 1 {
217 let p1 = &coords[i];
218 let p2 = &coords[i + 1];
219
220 let lat1 = p1.y * PI / 180.0;
222 let lon1 = p1.x * PI / 180.0;
223 let lat2 = p2.y * PI / 180.0;
224 let lon2 = p2.x * PI / 180.0;
225
226 if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
228 return Err(AlgorithmError::InvalidParameter {
229 parameter: "latitude",
230 message: "latitude must be between -90 and 90 degrees".to_string(),
231 });
232 }
233
234 let dlon = lon2 - lon1;
236
237 let dlon = if dlon > PI {
239 dlon - 2.0 * PI
240 } else if dlon < -PI {
241 dlon + 2.0 * PI
242 } else {
243 dlon
244 };
245
246 area += dlon * (lat1.sin() + lat2.sin());
248 }
249
250 let authalic_radius =
252 ((WGS84_A * WGS84_A + WGS84_B * WGS84_B / (1.0 - WGS84_F).sqrt()) / 2.0).sqrt();
253 let area_m2 = area.abs() * authalic_radius * authalic_radius / 2.0;
254
255 Ok(area_m2)
256}
257
258pub fn is_counter_clockwise(polygon: &Polygon) -> bool {
268 ring_area_planar(&polygon.exterior.coords) > 0.0
269}
270
271pub fn is_clockwise(polygon: &Polygon) -> bool {
281 ring_area_planar(&polygon.exterior.coords) < 0.0
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use oxigdal_core::vector::LineString;
288
289 fn create_square(size: f64) -> Result<Polygon> {
290 let coords = vec![
291 Coordinate::new_2d(0.0, 0.0),
292 Coordinate::new_2d(size, 0.0),
293 Coordinate::new_2d(size, size),
294 Coordinate::new_2d(0.0, size),
295 Coordinate::new_2d(0.0, 0.0),
296 ];
297 let exterior = LineString::new(coords).map_err(|e| AlgorithmError::Core(e))?;
298 Polygon::new(exterior, vec![]).map_err(|e| AlgorithmError::Core(e))
299 }
300
301 fn create_square_with_hole() -> Result<Polygon> {
302 let exterior_coords = vec![
303 Coordinate::new_2d(0.0, 0.0),
304 Coordinate::new_2d(10.0, 0.0),
305 Coordinate::new_2d(10.0, 10.0),
306 Coordinate::new_2d(0.0, 10.0),
307 Coordinate::new_2d(0.0, 0.0),
308 ];
309 let hole_coords = vec![
310 Coordinate::new_2d(2.0, 2.0),
311 Coordinate::new_2d(8.0, 2.0),
312 Coordinate::new_2d(8.0, 8.0),
313 Coordinate::new_2d(2.0, 8.0),
314 Coordinate::new_2d(2.0, 2.0),
315 ];
316
317 let exterior = LineString::new(exterior_coords).map_err(|e| AlgorithmError::Core(e))?;
318 let hole = LineString::new(hole_coords).map_err(|e| AlgorithmError::Core(e))?;
319
320 Polygon::new(exterior, vec![hole]).map_err(|e| AlgorithmError::Core(e))
321 }
322
323 #[test]
324 fn test_area_polygon_planar() {
325 let poly = create_square(10.0);
326 assert!(poly.is_ok());
327
328 if let Ok(p) = poly {
329 let result = area_polygon(&p, AreaMethod::Planar);
330 assert!(result.is_ok());
331
332 if let Ok(area) = result {
333 assert!((area - 100.0).abs() < 1e-10);
334 }
335 }
336 }
337
338 #[test]
339 fn test_area_polygon_with_hole() {
340 let poly = create_square_with_hole();
341 assert!(poly.is_ok());
342
343 if let Ok(p) = poly {
344 let result = area_polygon(&p, AreaMethod::Planar);
345 assert!(result.is_ok());
346
347 if let Ok(area) = result {
348 assert!((area - 64.0).abs() < 1e-10);
350 }
351 }
352 }
353
354 #[test]
355 fn test_area_polygon_signed() {
356 let poly = create_square(10.0);
357 assert!(poly.is_ok());
358
359 if let Ok(p) = poly {
360 let result = area_polygon(&p, AreaMethod::SignedPlanar);
361 assert!(result.is_ok());
362
363 if let Ok(area) = result {
364 assert!(area > 0.0);
366 assert!((area.abs() - 100.0).abs() < 1e-10);
367 }
368 }
369 }
370
371 #[test]
372 fn test_area_multipolygon() {
373 let poly1 = create_square(5.0);
374 let poly2 = create_square(3.0);
375
376 assert!(poly1.is_ok() && poly2.is_ok());
377
378 if let (Ok(p1), Ok(p2)) = (poly1, poly2) {
379 let mp = MultiPolygon::new(vec![p1, p2]);
380 let result = area_multipolygon(&mp, AreaMethod::Planar);
381 assert!(result.is_ok());
382
383 if let Ok(area) = result {
384 assert!((area - 34.0).abs() < 1e-10);
386 }
387 }
388 }
389
390 #[test]
391 fn test_area_geodetic_small_polygon() {
392 let coords = vec![
394 Coordinate::new_2d(0.0, 0.0),
395 Coordinate::new_2d(1.0, 0.0),
396 Coordinate::new_2d(1.0, 1.0),
397 Coordinate::new_2d(0.0, 1.0),
398 Coordinate::new_2d(0.0, 0.0),
399 ];
400 let exterior = LineString::new(coords);
401 assert!(exterior.is_ok());
402
403 if let Ok(ext) = exterior {
404 let poly = Polygon::new(ext, vec![]);
405 assert!(poly.is_ok());
406
407 if let Ok(p) = poly {
408 let result = area_polygon(&p, AreaMethod::Geodetic);
409 assert!(result.is_ok());
410
411 if let Ok(area) = result {
412 assert!(area > 1e10); assert!(area < 2e10); }
418 }
419 }
420 }
421
422 #[test]
423 fn test_is_counter_clockwise() {
424 let poly = create_square(10.0);
425 assert!(poly.is_ok());
426
427 if let Ok(p) = poly {
428 assert!(is_counter_clockwise(&p));
429 assert!(!is_clockwise(&p));
430 }
431 }
432
433 #[test]
434 fn test_ring_area_planar() {
435 let coords = vec![
436 Coordinate::new_2d(0.0, 0.0),
437 Coordinate::new_2d(10.0, 0.0),
438 Coordinate::new_2d(10.0, 10.0),
439 Coordinate::new_2d(0.0, 10.0),
440 Coordinate::new_2d(0.0, 0.0),
441 ];
442
443 let area = ring_area_planar(&coords);
444 assert!((area.abs() - 100.0).abs() < 1e-10);
445 }
446
447 #[test]
448 fn test_area_empty_multipolygon() {
449 let mp = MultiPolygon::empty();
450 let result = area_multipolygon(&mp, AreaMethod::Planar);
451 assert!(result.is_ok());
452
453 if let Ok(area) = result {
454 assert_eq!(area, 0.0);
455 }
456 }
457
458 #[test]
459 fn test_area_invalid_geometry() {
460 let point = Geometry::Point(oxigdal_core::vector::Point::new(0.0, 0.0));
461 let result = area(&point, AreaMethod::Planar);
462 assert!(result.is_err());
463 }
464
465 #[test]
466 fn test_geodetic_area_invalid_latitude() {
467 let coords = vec![
469 Coordinate::new_2d(0.0, 0.0),
470 Coordinate::new_2d(1.0, 0.0),
471 Coordinate::new_2d(1.0, 100.0), Coordinate::new_2d(0.0, 1.0),
473 Coordinate::new_2d(0.0, 0.0),
474 ];
475 let exterior = LineString::new(coords);
476 assert!(exterior.is_ok());
477
478 if let Ok(ext) = exterior {
479 let poly = Polygon::new(ext, vec![]);
480 assert!(poly.is_ok());
481
482 if let Ok(p) = poly {
483 let result = area_polygon(&p, AreaMethod::Geodetic);
484 assert!(result.is_err());
485 }
486 }
487 }
488}