1use geometry_cs::CoordinateSystem;
20use geometry_strategy::{
21 AreaStrategy, DefaultArea, DefaultAreaStrategy, ShoelaceArea, ShoelaceBoxArea,
22 ShoelaceMultiPolygonArea,
23};
24use geometry_trait::{Box, Geometry, MultiPolygon, Point, Polygon, Ring};
25
26type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
30
31#[inline]
45#[must_use]
46pub fn ring_area<R>(r: &R) -> <ShoelaceArea as AreaStrategy<R>>::Out
47where
48 R: Ring,
49 ShoelaceArea: AreaStrategy<R>,
50{
51 ShoelaceArea.area(r)
52}
53
54#[inline]
82#[must_use]
83pub fn area<P>(p: &P) -> <DefaultAreaStrategy<P> as AreaStrategy<P>>::Out
84where
85 P: Polygon,
86 Family<P>: DefaultArea<Family<P>>,
87 DefaultAreaStrategy<P>: AreaStrategy<P> + Default,
88{
89 DefaultAreaStrategy::<P>::default().area(p)
90}
91
92#[inline]
100#[must_use]
101#[allow(
102 clippy::needless_pass_by_value,
103 reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
104)]
105pub fn area_with<G, S>(g: &G, s: S) -> S::Out
106where
107 G: Geometry,
108 S: AreaStrategy<G>,
109{
110 s.area(g)
111}
112
113#[inline]
122#[must_use]
123pub fn box_area<B>(b: &B) -> <ShoelaceBoxArea as AreaStrategy<B>>::Out
124where
125 B: Box,
126 ShoelaceBoxArea: AreaStrategy<B>,
127{
128 ShoelaceBoxArea.area(b)
129}
130
131#[inline]
139#[must_use]
140pub fn multi_polygon_area<MPg>(mpg: &MPg) -> <ShoelaceMultiPolygonArea as AreaStrategy<MPg>>::Out
141where
142 MPg: MultiPolygon,
143 ShoelaceMultiPolygonArea: AreaStrategy<MPg>,
144{
145 ShoelaceMultiPolygonArea.area(mpg)
146}
147
148#[cfg(test)]
149mod tests {
150 #![allow(
155 clippy::float_cmp,
156 reason = "areas are compared with an explicit tolerance, not `==`"
157 )]
158
159 use super::{area, area_with, box_area, multi_polygon_area, ring_area};
160 use geometry_cs::Cartesian;
161 use geometry_model::{Box, MultiPolygon, Point2D, Polygon, Ring, polygon};
162
163 type P = Point2D<f64, Cartesian>;
164
165 #[test]
167 fn diamond_polygon_is_2() {
168 let p: Polygon<P> = polygon![[(1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (2.0, 0.0), (1.0, 1.0)]];
169 assert!((area(&p) - 2.0).abs() < 1e-12);
170 }
171
172 #[test]
174 fn pentagon_is_16() {
175 let p: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 7.0), (4.0, 2.0), (2.0, 0.0), (0.0, 0.0)]];
176 assert!((area(&p) - 16.0).abs() < 1e-12);
177 }
178
179 #[test]
181 fn ccw_unit_square_is_minus_1() {
182 let p: Polygon<P> = polygon![[(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)]];
183 assert!((area(&p) - -1.0).abs() < 1e-12);
184 }
185
186 #[test]
188 fn pentagon_with_hole_is_15() {
189 let p: Polygon<P> = polygon![
190 [(0.0, 0.0), (0.0, 7.0), (4.0, 2.0), (2.0, 0.0), (0.0, 0.0)],
191 [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)],
192 ];
193 assert!((area(&p) - 15.0).abs() < 1e-12);
194 }
195
196 #[test]
199 fn box_2x2_is_4() {
200 let b = Box::from_corners(
201 Point2D::<f64, Cartesian>::new(0.0, 0.0),
202 Point2D::<f64, Cartesian>::new(2.0, 2.0),
203 );
204 assert!((box_area(&b) - 4.0).abs() < 1e-12);
205 }
206
207 #[test]
209 fn ring_pentagon_is_16() {
210 let r: Ring<P> = Ring::from_vec(vec![
211 Point2D::new(0.0, 0.0),
212 Point2D::new(0.0, 7.0),
213 Point2D::new(4.0, 2.0),
214 Point2D::new(2.0, 0.0),
215 Point2D::new(0.0, 0.0),
216 ]);
217 assert!((ring_area(&r) - 16.0).abs() < 1e-12);
218 }
219
220 #[test]
222 fn quickstart_polygon_area_is_3_015() {
223 let p: Polygon<P> = polygon![[(2.0, 1.3), (4.1, 3.0), (5.3, 2.6), (2.9, 0.7), (2.0, 1.3)]];
224 assert!((area(&p) - 3.015).abs() < 1e-3);
225 }
226
227 #[test]
229 fn multi_polygon_two_unit_squares_is_2() {
230 let unit_at = |x: f64, y: f64| -> Polygon<P> {
231 polygon![[
232 (x, y),
233 (x, y + 1.0),
234 (x + 1.0, y + 1.0),
235 (x + 1.0, y),
236 (x, y)
237 ]]
238 };
239 let mpg: MultiPolygon<Polygon<P>> =
240 MultiPolygon::from_vec(vec![unit_at(0.0, 0.0), unit_at(5.0, 0.0)]);
241 assert!((multi_polygon_area(&mpg) - 2.0).abs() < 1e-12);
242 }
243
244 #[cfg(feature = "std")]
249 #[test]
250 fn spherical_area_dispatches_to_spherical_excess() {
251 use geometry_adapt::{Adapt, WithCs};
252 use geometry_cs::{Degree, Spherical};
253
254 type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
255 let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
256
257 let pg: Polygon<Sp> = Polygon::new(Ring::from_vec(vec![
258 sp(0., 0.),
259 sp(0., 90.),
260 sp(90., 0.),
261 sp(0., 0.),
262 ]));
263 let got = area(&pg);
264 let r = 6_371_000.0_f64;
266 let expected = core::f64::consts::FRAC_PI_2 * r * r;
267 assert!(
268 (got - expected).abs() / expected < 1e-6,
269 "got {got} expected {expected}"
270 );
271 }
272
273 #[cfg(feature = "std")]
277 #[test]
278 fn spherical_polygon_with_hole_subtracts_inner_area() {
279 use geometry_adapt::{Adapt, WithCs};
280 use geometry_cs::{Degree, Spherical};
281 use geometry_strategy::SphericalPolygonArea;
282
283 type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
284 let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
285
286 let outer = Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
289 let hole = Ring::from_vec(vec![sp(10., 10.), sp(30., 10.), sp(20., 20.), sp(10., 10.)]);
290 let pg: Polygon<Sp> = Polygon::with_inners(outer, vec![hole]);
291 let with_hole = area_with(&pg, SphericalPolygonArea::UNIT);
292 let whole = core::f64::consts::FRAC_PI_2;
293 assert!(with_hole < whole, "hole must reduce area: {with_hole}");
294 assert!(with_hole > 0.0, "still positive: {with_hole}");
295 }
296
297 #[cfg(feature = "std")]
301 #[test]
302 fn spherical_open_ring_closes_implicitly() {
303 use geometry_adapt::{Adapt, WithCs};
304 use geometry_cs::{Degree, Spherical};
305 use geometry_strategy::SphericalArea;
306
307 type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
308 let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
309
310 let closed: Ring<Sp> =
311 Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
312 let open: Ring<Sp, true, false> =
313 Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.)]);
314 let a_closed = area_with(&closed, SphericalArea::UNIT);
315 let a_open = area_with(&open, SphericalArea::UNIT);
316 assert!((a_closed - a_open).abs() < 1e-9, "{a_closed} vs {a_open}");
317 }
318
319 #[cfg(feature = "std")]
323 #[test]
324 fn spherical_antimeridian_crossing_normalises_dlon() {
325 use geometry_adapt::{Adapt, WithCs};
326 use geometry_cs::{Degree, Spherical};
327 use geometry_strategy::SphericalArea;
328
329 type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
330 let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
331
332 let r: Ring<Sp> = Ring::from_vec(vec![
333 sp(170., 0.),
334 sp(170., 10.),
335 sp(-170., 10.),
336 sp(-170., 0.),
337 sp(170., 0.),
338 ]);
339 let got = area_with(&r, SphericalArea::UNIT).abs();
340 assert!(got < 0.2, "expected a small sliver area, got {got}");
342 }
343
344 #[cfg(feature = "std")]
348 #[test]
349 fn geographic_area_dispatches_to_authalic_sphere() {
350 use geometry_adapt::{Adapt, WithCs};
351 use geometry_cs::{Degree, Geographic};
352
353 type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
354 let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
355
356 let pg: Polygon<Gg> = Polygon::new(Ring::from_vec(vec![
357 gg(0., 0.),
358 gg(1., 0.),
359 gg(1., 1.),
360 gg(0., 1.),
361 gg(0., 0.),
362 ]));
363 let got = area(&pg).abs();
364 let expected = 12_309e6;
365 assert!((got - expected).abs() / expected < 0.02);
366 }
367
368 #[cfg(feature = "std")]
372 #[test]
373 fn geographic_polygon_with_hole_subtracts_hole_area() {
374 use geometry_adapt::{Adapt, WithCs};
375 use geometry_cs::{Degree, Geographic};
376
377 type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
378 let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
379
380 let outer = Ring::from_vec(vec![
381 gg(0., 0.),
382 gg(1., 0.),
383 gg(1., 1.),
384 gg(0., 1.),
385 gg(0., 0.),
386 ]);
387 let hole: Ring<Gg> = Ring::from_vec(vec![
389 gg(0.2, 0.2),
390 gg(0.2, 0.8),
391 gg(0.8, 0.8),
392 gg(0.8, 0.2),
393 gg(0.2, 0.2),
394 ]);
395 let outer_only = area(&Polygon::new(outer.clone())).abs();
396 let hole_alone = area(&Polygon::new(hole.clone())).abs();
397 let holed = Polygon::with_inners(outer, vec![hole]);
398 let got = area(&holed).abs();
399 let expected = outer_only - hole_alone;
400 assert!(
401 (got - expected).abs() / expected < 1e-9,
402 "got {got}, expected outer − hole = {expected}"
403 );
404 }
405
406 #[cfg(feature = "std")]
410 #[test]
411 fn geographic_open_ring_closes_implicitly() {
412 use geometry_adapt::{Adapt, WithCs};
413 use geometry_cs::{Degree, Geographic};
414
415 type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
416 let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
417
418 let open: Polygon<Gg, true, false> = Polygon::new(Ring::from_vec(vec![
419 gg(0., 0.),
420 gg(1., 0.),
421 gg(1., 1.),
422 gg(0., 1.),
423 ]));
424 let closed: Polygon<Gg> = Polygon::new(Ring::from_vec(vec![
425 gg(0., 0.),
426 gg(1., 0.),
427 gg(1., 1.),
428 gg(0., 1.),
429 gg(0., 0.),
430 ]));
431 let got_open = area(&open).abs();
432 let got_closed = area(&closed).abs();
433 assert!(
434 (got_open - got_closed).abs() / got_closed < 1e-12,
435 "open {got_open} != closed {got_closed}"
436 );
437 }
438
439 #[cfg(feature = "std")]
443 #[test]
444 fn geographic_ccw_declared_polygon_negates_sign() {
445 use geometry_adapt::{Adapt, WithCs};
446 use geometry_cs::{Degree, Geographic};
447
448 type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
449 let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
450
451 let verts = vec![gg(0., 0.), gg(1., 0.), gg(1., 1.), gg(0., 1.), gg(0., 0.)];
452 let cw: Polygon<Gg, true> = Polygon::new(Ring::from_vec(verts.clone()));
453 let ccw: Polygon<Gg, false> = Polygon::new(Ring::from_vec(verts));
454 let sum = area(&cw) + area(&ccw);
455 assert!(
456 sum.abs() < 1e-3,
457 "signed areas do not cancel: cw + ccw = {sum}"
458 );
459 assert!(area(&cw).abs() > 1e9, "area unexpectedly tiny");
460 }
461}