geometry_strategy/area.rs
1//! Strategy for computing the area of a Cartesian geometry.
2//!
3//! Mirrors three pieces of Boost.Geometry that collaborate to make
4//! `boost::geometry::area(g)` work for any ring / polygon / box /
5//! multi-polygon in any coordinate system:
6//!
7//! * `boost/geometry/strategies/area/services.hpp` — the
8//! `services::default_strategy<G>` metafunction that picks the
9//! per-CS area strategy.
10//! * `boost/geometry/strategies/area/cartesian.hpp` —
11//! `strategies::area::cartesian<>` plus its
12//! `services::default_strategy<Geometry, cartesian_tag>`
13//! specialisation; the umbrella strategy hands out
14//! `strategy::area::cartesian<>` for ring / polygon geometries and
15//! `strategy::area::cartesian_box<>` for boxes.
16//! * `boost/geometry/strategy/cartesian/area.hpp:91-120` — the
17//! trapezoidal-rule accumulation `(x1 + x2) * (y1 - y2)` summed over
18//! consecutive segments and halved at the end. The Boost code wraps
19//! the ring in `closed_clockwise_view` first so a counter-clockwise
20//! declared ring traversed in its declared order still produces a
21//! positive area; the Rust port mirrors that by flipping the sign
22//! when [`PointOrder::CounterClockwise`] is declared.
23//!
24//! T34 lands the Cartesian implementation only — Boost's Spherical /
25//! Geographic area strategies arrive alongside the Haversine /
26//! geographic distance work in later tasks (T40+).
27//!
28//! # Coherence note
29//!
30//! Rust's coherence rules cannot prove that no single type is both a
31//! [`Ring`] and a [`Polygon`] (or a [`Box`] / [`MultiPolygon`]) at the
32//! same time, so a single `ShoelaceArea` carrying four
33//! `impl AreaStrategy<G>` blocks keyed off `G: Ring`, `G: Polygon`,
34//! `G: Box`, `G: MultiPolygon` is rejected as overlapping. Boost
35//! sidesteps this with tag dispatch (`strategy::area::cartesian` vs.
36//! `strategy::area::cartesian_box`, plus the per-tag `dispatch::area`
37//! arms in `algorithms/area.hpp:131-187`); we mirror that split with
38//! four sibling unit-structs below, each implementing
39//! [`AreaStrategy`] for exactly one geometry kind.
40
41use geometry_coords::CoordinateScalar;
42use geometry_cs::{CartesianFamily, CoordinateSystem, GeographicFamily, SphericalFamily};
43use geometry_tag::SameAs;
44use geometry_trait::{
45 Box, Closure, Geometry, MultiPolygon, Point, PointOrder, Polygon, Ring, corner,
46};
47
48/// A strategy for computing the area of a geometry.
49///
50/// Mirrors the per-CS area-strategy concept declared in
51/// `boost/geometry/strategies/area/services.hpp` and refined per
52/// coordinate system in `strategies/area/{cartesian,spherical,
53/// geographic}.hpp`. The Boost concept exposes a stateful `apply(p1,
54/// p2, state)` accumulator plus a final `result(state)` reduction;
55/// the Rust analogue collapses the two phases into a single method
56/// [`AreaStrategy::area`] keyed on the geometry type, because the
57/// per-segment walk shape is identical for every CS — only the
58/// per-segment kernel changes.
59///
60/// # Associated items
61///
62/// * [`Self::Out`] — the scalar the area comes back as.
63/// Equivalent to Boost's `area_result<Geometry, Strategies>::type`
64/// (`algorithms/area_result.hpp`); typically the coordinate scalar
65/// of `G`'s point type.
66pub trait AreaStrategy<G: Geometry> {
67 /// The output scalar type. Typically the geometry's coordinate
68 /// scalar. Mirrors `area_result<G, Strategies>::type` from
69 /// `algorithms/area_result.hpp`.
70 type Out: CoordinateScalar;
71
72 /// Compute the area of `g`.
73 ///
74 /// Mirrors the `result(strategy.apply(...))` pair from
75 /// `algorithms/area.hpp:111-116` together with the CS-specific
76 /// `strategy::area::cartesian::apply` walk at
77 /// `strategy/cartesian/area.hpp:91-112`.
78 fn area(&self, g: &G) -> Self::Out;
79}
80
81/// Cartesian shoelace area for a [`Ring`].
82///
83/// Mirrors `boost::geometry::strategy::area::cartesian<>` from
84/// `strategy/cartesian/area.hpp:50-120` applied to a ring through the
85/// `dispatch::area<Ring, ring_tag>` arm at `algorithms/area.hpp:154-157`.
86///
87/// Sign convention follows Boost: rings whose vertices match the
88/// declared [`PointOrder`] yield a positive area, rings traversed in
89/// the opposite direction yield a negative area
90/// (`test/algorithms/area/area.cpp:63-64`).
91#[derive(Debug, Default, Clone, Copy)]
92pub struct ShoelaceArea;
93
94/// Cartesian shoelace area for a [`Polygon`] — outer ring area minus
95/// the sum of interior-ring areas.
96///
97/// Mirrors the `dispatch::area<Polygon, polygon_tag>` arm at
98/// `algorithms/area.hpp:160-172`, which inherits from
99/// `detail::calculate_polygon_sum` and delegates the per-ring work to
100/// the same `ring_area` used by [`ShoelaceArea`]. The split into a
101/// dedicated strategy type is a Rust coherence concession; see the
102/// module-level documentation.
103#[derive(Debug, Default, Clone, Copy)]
104pub struct ShoelacePolygonArea;
105
106/// Cartesian area for a [`Box`]: `(xmax - xmin) * (ymax - ymin)`.
107///
108/// Mirrors `boost::geometry::strategy::area::cartesian_box<>` from
109/// `strategy/cartesian/area_box.hpp:28-48` applied through the
110/// `dispatch::area<Box, box_tag>` arm at
111/// `algorithms/area.hpp:149-151`.
112#[derive(Debug, Default, Clone, Copy)]
113pub struct ShoelaceBoxArea;
114
115/// Cartesian shoelace area for a [`MultiPolygon`] — sum of the areas
116/// of its member polygons.
117///
118/// Mirrors the `dispatch::area<MultiGeometry, multi_polygon_tag>` arm
119/// at `algorithms/area.hpp:175-187`, which inherits from
120/// `detail::multi_sum` and delegates the per-polygon work to the same
121/// `polygon_area` used by [`ShoelacePolygonArea`].
122#[derive(Debug, Default, Clone, Copy)]
123pub struct ShoelaceMultiPolygonArea;
124
125// ---- Ring ------------------------------------------------------------
126//
127// Mirrors the `dispatch::area<Ring, ring_tag>` arm at
128// `algorithms/area.hpp:154-157`, which inherits from
129// `detail::area::ring_area::apply` (`algorithms/area.hpp:82-118`).
130// The Boost code wraps the ring in `closed_clockwise_view` first so
131// that a counter-clockwise declared ring traversed in its declared
132// order still feeds clockwise vertices to the strategy; we mirror
133// that by negating the accumulator when [`PointOrder::CounterClockwise`]
134// is declared (the arithmetic equivalent of reversing the iteration).
135// The "open ring closes itself" half of `closed_clockwise_view` is
136// mirrored by an explicit `last -> first` step when [`Closure::Open`].
137
138impl<R> AreaStrategy<R> for ShoelaceArea
139where
140 R: Ring,
141 <R::Point as Point>::Cs: CoordinateSystem,
142 <<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
143{
144 type Out = <R::Point as Point>::Scalar;
145
146 #[inline]
147 fn area(&self, r: &R) -> Self::Out {
148 let acc = shoelace_accumulator::<R>(r);
149 let two = <Self::Out as CoordinateScalar>::ONE + <Self::Out as CoordinateScalar>::ONE;
150 let half = acc / two;
151 match r.point_order() {
152 PointOrder::Clockwise => half,
153 PointOrder::CounterClockwise => -half,
154 }
155 }
156}
157
158// ---- Polygon ---------------------------------------------------------
159//
160// Mirrors the `dispatch::area<Polygon, polygon_tag>` arm at
161// `algorithms/area.hpp:160-172`. Boost spells the recursion as
162// `calculate_polygon_sum::apply<…, ring_area>(polygon, strategy)`,
163// summing the (signed) area of every ring — exterior plus interiors.
164// Boost's signed-area convention means the interior rings, which by
165// convention are wound opposite the exterior, already arrive with the
166// opposite sign, so a *sum* of ring areas gives the polygon area; the
167// Rust mirror is exactly the same arithmetic.
168
169impl<P> AreaStrategy<P> for ShoelacePolygonArea
170where
171 P: Polygon,
172 ShoelaceArea: AreaStrategy<P::Ring, Out = <P::Point as Point>::Scalar>,
173{
174 type Out = <P::Point as Point>::Scalar;
175
176 #[inline]
177 fn area(&self, p: &P) -> Self::Out {
178 let mut total = ShoelaceArea.area(p.exterior());
179 for inner in p.interiors() {
180 total = total + ShoelaceArea.area(inner);
181 }
182 total
183 }
184}
185
186// ---- Box -------------------------------------------------------------
187//
188// Mirrors the `dispatch::area<Box, box_tag>` arm at
189// `algorithms/area.hpp:149-151`, which inherits from
190// `detail::area::box_area::apply`. Boost asserts a 2D box and computes
191// `(xmax - xmin) * (ymax - ymin)` (`strategy/cartesian/area_box.hpp:41-47`).
192// The Rust port enforces 2D via the const-generic bound on
193// [`corner::MIN`] / [`corner::MAX`]; higher-dimensional boxes simply
194// will not see this impl matched because the formula only reads
195// dimensions 0 and 1.
196
197impl<B> AreaStrategy<B> for ShoelaceBoxArea
198where
199 B: Box,
200 <B::Point as Point>::Cs: CoordinateSystem,
201 <<B::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
202{
203 type Out = <B::Point as Point>::Scalar;
204
205 #[inline]
206 fn area(&self, b: &B) -> Self::Out {
207 let xmin = b.get_indexed::<{ corner::MIN }, 0>();
208 let ymin = b.get_indexed::<{ corner::MIN }, 1>();
209 let xmax = b.get_indexed::<{ corner::MAX }, 0>();
210 let ymax = b.get_indexed::<{ corner::MAX }, 1>();
211 (xmax - xmin) * (ymax - ymin)
212 }
213}
214
215// ---- MultiPolygon ----------------------------------------------------
216//
217// Mirrors the `dispatch::area<MultiGeometry, multi_polygon_tag>` arm
218// at `algorithms/area.hpp:175-187`. Boost spells the recursion as
219// `multi_sum::apply<…, area<polygon>>(multi, strategy)`, summing the
220// signed area of every member polygon.
221
222impl<MPg> AreaStrategy<MPg> for ShoelaceMultiPolygonArea
223where
224 MPg: MultiPolygon,
225 ShoelacePolygonArea: AreaStrategy<MPg::ItemPolygon, Out = <MPg::Point as Point>::Scalar>,
226{
227 type Out = <MPg::Point as Point>::Scalar;
228
229 #[inline]
230 fn area(&self, mpg: &MPg) -> Self::Out {
231 let mut total = <Self::Out as CoordinateScalar>::ZERO;
232 for p in mpg.polygons() {
233 total = total + ShoelacePolygonArea.area(p);
234 }
235 total
236 }
237}
238
239/// Sum `(x_i + x_{i+1}) * (y_i - y_{i+1})` over the consecutive
240/// vertex pairs of `r`. For an open ring the implicit `last -> first`
241/// closing pair is added explicitly, mirroring Boost's
242/// `closed_clockwise_view` closure half at
243/// `algorithms/area.hpp:104` and `strategy/cartesian/area.hpp:109-112`.
244///
245/// Returns `R::Point::Scalar::ZERO` for rings with fewer than two
246/// vertices — same as Boost's `ring_area::apply` early return at
247/// `algorithms/area.hpp:99-102` (`detail::minimum_ring_size`).
248#[inline]
249fn shoelace_accumulator<R>(r: &R) -> <R::Point as Point>::Scalar
250where
251 R: Ring,
252{
253 let mut acc = <<R::Point as Point>::Scalar as CoordinateScalar>::ZERO;
254 let it = r.points();
255 let next = it.clone().skip(1);
256 for (a, b) in it.zip(next) {
257 acc = acc + segment_term::<R::Point>(a, b);
258 }
259 if matches!(r.closure(), Closure::Open) {
260 // Open ring leaves the closing edge implicit — add it
261 // explicitly. Mirrors the `closed_clockwise_view` closure half
262 // at `views/detail/closed_clockwise_view.hpp`.
263 let mut it = r.points();
264 if let Some(first) = it.next() {
265 if let Some(last) = r.points().last() {
266 acc = acc + segment_term::<R::Point>(last, first);
267 }
268 }
269 }
270 acc
271}
272
273/// One trapezoidal-rule term: `(x_a + x_b) * (y_a - y_b)`.
274///
275/// Mirrors the per-segment kernel at
276/// `strategy/cartesian/area.hpp:110-111`. Boost notes that this
277/// formulation loses less precision than the naive
278/// `x_a * y_b - x_b * y_a` cross product at large coordinate values
279/// (Boost trac #11928 cited in the same header).
280#[inline]
281fn segment_term<P>(a: &P, b: &P) -> P::Scalar
282where
283 P: Point,
284{
285 (a.get::<0>() + b.get::<0>()) * (a.get::<1>() - b.get::<1>())
286}
287
288// ---- Default area strategy per CS family ----------------------------
289
290/// "Which (polygon) area strategy do we pick by default for this CS
291/// family?"
292///
293/// Mirrors v1's [`DefaultDistance`](crate::distance::DefaultDistance)
294/// and [`DefaultLength`](crate::length::DefaultLength) — the Rust
295/// analogue of Boost's `services::default_strategy<Geometry, cs_tag>`
296/// in `strategies/area/services.hpp`, specialised per CS in
297/// `strategies/area/{cartesian,spherical,geographic}.hpp`.
298///
299/// Keyed on the *polygon* area strategy, since the `area(&polygon)`
300/// free function is the entry point that dispatches through it:
301///
302/// ```ignore
303/// impl DefaultArea<CartesianFamily> for CartesianFamily { type Strategy = ShoelacePolygonArea; }
304/// impl DefaultArea<SphericalFamily> for SphericalFamily { type Strategy = SphericalPolygonArea; }
305/// impl DefaultArea<GeographicFamily> for GeographicFamily { type Strategy = GeographicPolygonArea; }
306/// ```
307pub trait DefaultArea<Family> {
308 /// The area strategy chosen for this family. Must implement
309 /// [`Default`] because the free-function `area(g)` builds it
310 /// without arguments.
311 type Strategy: Default;
312}
313
314/// Cartesian family defaults to [`ShoelacePolygonArea`].
315impl DefaultArea<CartesianFamily> for CartesianFamily {
316 type Strategy = ShoelacePolygonArea;
317}
318
319/// Spherical family defaults to [`SphericalPolygonArea`](crate::spherical::SphericalPolygonArea).
320impl DefaultArea<SphericalFamily> for SphericalFamily {
321 type Strategy = crate::spherical::SphericalPolygonArea;
322}
323
324/// Geographic family defaults to [`GeographicPolygonArea`](crate::geographic::GeographicPolygonArea)
325/// — the authalic-sphere approximation (see its docs for the precision
326/// caveat).
327impl DefaultArea<GeographicFamily> for GeographicFamily {
328 type Strategy = crate::geographic::GeographicPolygonArea;
329}
330
331/// Type alias resolving the default area strategy for geometry `G` by
332/// walking `G -> G::Point -> Cs -> Family -> DefaultArea::Strategy`.
333///
334/// Mirrors [`DefaultDistanceStrategy`](crate::distance::DefaultDistanceStrategy)
335/// for the area algorithm.
336pub type DefaultAreaStrategy<G> =
337 <<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultArea<
338 <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
339 >>::Strategy;
340
341#[cfg(test)]
342mod tests {
343 //! Reference values from `geometry/test/algorithms/area/area.cpp`
344 //! (lines 45-64). Each test cites the source it mirrors.
345
346 use super::{
347 AreaStrategy, ShoelaceArea, ShoelaceBoxArea, ShoelaceMultiPolygonArea, ShoelacePolygonArea,
348 };
349 use geometry_cs::Cartesian;
350 use geometry_model::{Box, MultiPolygon, Point2D, Polygon, Ring, polygon};
351
352 type P = Point2D<f64, Cartesian>;
353
354 /// `area.cpp:45` — rotated unit square, area = 2.
355 #[test]
356 fn ring_diamond_is_2() {
357 let r: Ring<P> = Ring::from_vec(vec![
358 Point2D::new(1.0, 1.0),
359 Point2D::new(2.0, 2.0),
360 Point2D::new(3.0, 1.0),
361 Point2D::new(2.0, 0.0),
362 Point2D::new(1.0, 1.0),
363 ]);
364 let got = ShoelaceArea.area(&r);
365 assert!((got - 2.0).abs() < 1e-12);
366 }
367
368 /// `area.cpp:47, 63` — pentagon, area = 16.
369 #[test]
370 fn ring_pentagon_is_16() {
371 let r: Ring<P> = Ring::from_vec(vec![
372 Point2D::new(0.0, 0.0),
373 Point2D::new(0.0, 7.0),
374 Point2D::new(4.0, 2.0),
375 Point2D::new(2.0, 0.0),
376 Point2D::new(0.0, 0.0),
377 ]);
378 let got = ShoelaceArea.area(&r);
379 assert!((got - 16.0).abs() < 1e-12);
380 }
381
382 /// `area.cpp:64` — same pentagon traversed in the opposite
383 /// direction, declared as a default (CW) ring → area = -16.
384 #[test]
385 fn ring_wrongly_ordered_is_minus_16() {
386 let r: Ring<P> = Ring::from_vec(vec![
387 Point2D::new(0.0, 0.0),
388 Point2D::new(2.0, 0.0),
389 Point2D::new(4.0, 2.0),
390 Point2D::new(0.0, 7.0),
391 Point2D::new(0.0, 0.0),
392 ]);
393 let got = ShoelaceArea.area(&r);
394 assert!((got - -16.0).abs() < 1e-12);
395 }
396
397 /// `area.cpp:48` — unit-square vertices in CCW order on a
398 /// default-CW polygon → area = -1.
399 #[test]
400 fn polygon_ccw_unit_square_is_minus_1() {
401 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)]];
402 let got = ShoelacePolygonArea.area(&p);
403 assert!((got - -1.0).abs() < 1e-12);
404 }
405
406 /// `area.cpp:49` — pentagon (area 16) minus a unit-square hole
407 /// (signed area -1) = 15.
408 #[test]
409 fn polygon_pentagon_with_hole_is_15() {
410 let outer: Ring<P> = Ring::from_vec(vec![
411 Point2D::new(0.0, 0.0),
412 Point2D::new(0.0, 7.0),
413 Point2D::new(4.0, 2.0),
414 Point2D::new(2.0, 0.0),
415 Point2D::new(0.0, 0.0),
416 ]);
417 let hole: Ring<P> = Ring::from_vec(vec![
418 Point2D::new(1.0, 1.0),
419 Point2D::new(2.0, 1.0),
420 Point2D::new(2.0, 2.0),
421 Point2D::new(1.0, 2.0),
422 Point2D::new(1.0, 1.0),
423 ]);
424 let mut p: Polygon<P> = Polygon::new(outer);
425 p.inners.push(hole);
426 let got = ShoelacePolygonArea.area(&p);
427 assert!((got - 15.0).abs() < 1e-12);
428 }
429
430 /// `area.cpp:56-57` — both orderings of the same box give the
431 /// same area (4). The Cartesian box formula is sign-blind.
432 #[test]
433 fn box_2x2_is_4() {
434 let b = Box::from_corners(
435 Point2D::<f64, Cartesian>::new(0.0, 0.0),
436 Point2D::new(2.0, 2.0),
437 );
438 let got = ShoelaceBoxArea.area(&b);
439 assert!((got - 4.0).abs() < 1e-12);
440 }
441
442 /// Multi-polygon: two disjoint CW unit squares → area = 2.
443 #[test]
444 fn multipolygon_two_unit_squares_is_2() {
445 // CW: (x,y) -> (x,y+1) -> (x+1,y+1) -> (x+1,y) -> (x,y).
446 let unit_at = |x: f64, y: f64| -> Polygon<P> {
447 polygon![[
448 (x, y),
449 (x, y + 1.0),
450 (x + 1.0, y + 1.0),
451 (x + 1.0, y),
452 (x, y)
453 ]]
454 };
455 let mpg: MultiPolygon<Polygon<P>> =
456 MultiPolygon::from_vec(vec![unit_at(0.0, 0.0), unit_at(5.0, 0.0)]);
457 let got = ShoelaceMultiPolygonArea.area(&mpg);
458 assert!((got - 2.0).abs() < 1e-12);
459 }
460
461 /// Open ring of a 2x2 square (no repeated closing vertex): the
462 /// strategy must add the implicit last->first edge so the area
463 /// still comes out to 4.
464 #[test]
465 fn open_ring_2x2_square_is_4() {
466 let mut r = Ring::<P, true, false>::new();
467 r.push(Point2D::new(0.0, 0.0));
468 r.push(Point2D::new(0.0, 2.0));
469 r.push(Point2D::new(2.0, 2.0));
470 r.push(Point2D::new(2.0, 0.0));
471 let got = ShoelaceArea.area(&r);
472 assert!((got - 4.0).abs() < 1e-12);
473 }
474
475 /// Counter-clockwise declared ring traversed CCW (matching its
476 /// declared order) → positive area. Mirrors
477 /// `area.cpp:69-73` whose diamond on `polygon<P, false>` yields 2.
478 #[test]
479 fn ccw_declared_ccw_traversed_diamond_is_2() {
480 let r: Ring<P, false> = Ring::from_vec(vec![
481 Point2D::new(1.0, 0.0),
482 Point2D::new(0.0, 1.0),
483 Point2D::new(-1.0, 0.0),
484 Point2D::new(0.0, -1.0),
485 Point2D::new(1.0, 0.0),
486 ]);
487 let got = ShoelaceArea.area(&r);
488 assert!((got - 2.0).abs() < 1e-12);
489 }
490
491 // KC1.T2 witness: proves this strategy accepts a geometry whose
492 // `Point` is read-only (need not implement `PointMut`). If it
493 // compiles, the read-only bound is locked.
494 fn _accepts_readonly_point<G, S>(s: &S, g: &G) -> S::Out
495 where
496 G: geometry_trait::Geometry,
497 <G as geometry_trait::Geometry>::Point: geometry_trait::Point,
498 S: AreaStrategy<G>,
499 {
500 s.area(g)
501 }
502}