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 points = r.points();
264 if let Some(first) = points.next() {
265 let last = points.last().unwrap_or(first);
266 acc = acc + segment_term::<R::Point>(last, first);
267 }
268 }
269 acc
270}
271
272/// One trapezoidal-rule term: `(x_a + x_b) * (y_a - y_b)`.
273///
274/// Mirrors the per-segment kernel at
275/// `strategy/cartesian/area.hpp:110-111`. Boost notes that this
276/// formulation loses less precision than the naive
277/// `x_a * y_b - x_b * y_a` cross product at large coordinate values
278/// (Boost trac #11928 cited in the same header).
279#[inline]
280fn segment_term<P>(a: &P, b: &P) -> P::Scalar
281where
282 P: Point,
283{
284 (a.get::<0>() + b.get::<0>()) * (a.get::<1>() - b.get::<1>())
285}
286
287// ---- Default area strategy per CS family ----------------------------
288
289/// "Which (polygon) area strategy do we pick by default for this CS
290/// family?"
291///
292/// Mirrors v1's [`DefaultDistance`](crate::distance::DefaultDistance)
293/// and [`DefaultLength`](crate::length::DefaultLength) — the Rust
294/// analogue of Boost's `services::default_strategy<Geometry, cs_tag>`
295/// in `strategies/area/services.hpp`, specialised per CS in
296/// `strategies/area/{cartesian,spherical,geographic}.hpp`.
297///
298/// Keyed on the *polygon* area strategy, since the `area(&polygon)`
299/// free function is the entry point that dispatches through it:
300///
301/// ```ignore
302/// impl DefaultArea<CartesianFamily> for CartesianFamily { type Strategy = ShoelacePolygonArea; }
303/// impl DefaultArea<SphericalFamily> for SphericalFamily { type Strategy = SphericalPolygonArea; }
304/// impl DefaultArea<GeographicFamily> for GeographicFamily { type Strategy = GeographicPolygonArea; }
305/// ```
306pub trait DefaultArea<Family> {
307 /// The area strategy chosen for this family. Must implement
308 /// [`Default`] because the free-function `area(g)` builds it
309 /// without arguments.
310 type Strategy: Default;
311}
312
313/// Cartesian family defaults to [`ShoelacePolygonArea`].
314impl DefaultArea<CartesianFamily> for CartesianFamily {
315 type Strategy = ShoelacePolygonArea;
316}
317
318/// Spherical family defaults to [`SphericalPolygonArea`](crate::spherical::SphericalPolygonArea).
319impl DefaultArea<SphericalFamily> for SphericalFamily {
320 type Strategy = crate::spherical::SphericalPolygonArea;
321}
322
323/// Geographic family defaults to [`GeographicPolygonArea`](crate::geographic::GeographicPolygonArea)
324/// — the authalic-sphere approximation (see its docs for the precision
325/// caveat).
326impl DefaultArea<GeographicFamily> for GeographicFamily {
327 type Strategy = crate::geographic::GeographicPolygonArea;
328}
329
330/// Type alias resolving the default area strategy for geometry `G` by
331/// walking `G -> G::Point -> Cs -> Family -> DefaultArea::Strategy`.
332///
333/// Mirrors [`DefaultDistanceStrategy`](crate::distance::DefaultDistanceStrategy)
334/// for the area algorithm.
335pub type DefaultAreaStrategy<G> =
336 <<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultArea<
337 <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
338 >>::Strategy;
339
340#[cfg(test)]
341mod tests {
342 //! Reference values from `geometry/test/algorithms/area/area.cpp`
343 //! (lines 45-64). Each test cites the source it mirrors.
344
345 use super::{
346 AreaStrategy, ShoelaceArea, ShoelaceBoxArea, ShoelaceMultiPolygonArea, ShoelacePolygonArea,
347 };
348 use geometry_cs::Cartesian;
349 use geometry_model::{Box, MultiPolygon, Point2D, Polygon, Ring, polygon};
350
351 type P = Point2D<f64, Cartesian>;
352
353 /// `area.cpp:45` — rotated unit square, area = 2.
354 #[test]
355 fn ring_diamond_is_2() {
356 let r: Ring<P> = Ring::from_vec(vec![
357 Point2D::new(1.0, 1.0),
358 Point2D::new(2.0, 2.0),
359 Point2D::new(3.0, 1.0),
360 Point2D::new(2.0, 0.0),
361 Point2D::new(1.0, 1.0),
362 ]);
363 let got = accepts_readonly_point(&ShoelaceArea, &r);
364 assert!((got - 2.0).abs() < 1e-12);
365 }
366
367 /// `area.cpp:47, 63` — pentagon, area = 16.
368 #[test]
369 fn ring_pentagon_is_16() {
370 let r: Ring<P> = Ring::from_vec(vec![
371 Point2D::new(0.0, 0.0),
372 Point2D::new(0.0, 7.0),
373 Point2D::new(4.0, 2.0),
374 Point2D::new(2.0, 0.0),
375 Point2D::new(0.0, 0.0),
376 ]);
377 let got = ShoelaceArea.area(&r);
378 assert!((got - 16.0).abs() < 1e-12);
379 }
380
381 /// `area.cpp:64` — same pentagon traversed in the opposite
382 /// direction, declared as a default (CW) ring → area = -16.
383 #[test]
384 fn ring_wrongly_ordered_is_minus_16() {
385 let r: Ring<P> = Ring::from_vec(vec![
386 Point2D::new(0.0, 0.0),
387 Point2D::new(2.0, 0.0),
388 Point2D::new(4.0, 2.0),
389 Point2D::new(0.0, 7.0),
390 Point2D::new(0.0, 0.0),
391 ]);
392 let got = ShoelaceArea.area(&r);
393 assert!((got - -16.0).abs() < 1e-12);
394 }
395
396 /// `area.cpp:48` — unit-square vertices in CCW order on a
397 /// default-CW polygon → area = -1.
398 #[test]
399 fn polygon_ccw_unit_square_is_minus_1() {
400 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)]];
401 let got = ShoelacePolygonArea.area(&p);
402 assert!((got - -1.0).abs() < 1e-12);
403 }
404
405 /// `area.cpp:49` — pentagon (area 16) minus a unit-square hole
406 /// (signed area -1) = 15.
407 #[test]
408 fn polygon_pentagon_with_hole_is_15() {
409 let outer: Ring<P> = Ring::from_vec(vec![
410 Point2D::new(0.0, 0.0),
411 Point2D::new(0.0, 7.0),
412 Point2D::new(4.0, 2.0),
413 Point2D::new(2.0, 0.0),
414 Point2D::new(0.0, 0.0),
415 ]);
416 let hole: Ring<P> = Ring::from_vec(vec![
417 Point2D::new(1.0, 1.0),
418 Point2D::new(2.0, 1.0),
419 Point2D::new(2.0, 2.0),
420 Point2D::new(1.0, 2.0),
421 Point2D::new(1.0, 1.0),
422 ]);
423 let mut p: Polygon<P> = Polygon::new(outer);
424 p.inners.push(hole);
425 let got = ShoelacePolygonArea.area(&p);
426 assert!((got - 15.0).abs() < 1e-12);
427 }
428
429 /// `area.cpp:56-57` — both orderings of the same box give the
430 /// same area (4). The Cartesian box formula is sign-blind.
431 #[test]
432 fn box_2x2_is_4() {
433 let b = Box::from_corners(
434 Point2D::<f64, Cartesian>::new(0.0, 0.0),
435 Point2D::new(2.0, 2.0),
436 );
437 let got = ShoelaceBoxArea.area(&b);
438 assert!((got - 4.0).abs() < 1e-12);
439 }
440
441 /// Multi-polygon: two disjoint CW unit squares → area = 2.
442 #[test]
443 fn multipolygon_two_unit_squares_is_2() {
444 // CW: (x,y) -> (x,y+1) -> (x+1,y+1) -> (x+1,y) -> (x,y).
445 let unit_at = |x: f64, y: f64| -> Polygon<P> {
446 polygon![[
447 (x, y),
448 (x, y + 1.0),
449 (x + 1.0, y + 1.0),
450 (x + 1.0, y),
451 (x, y)
452 ]]
453 };
454 let mpg: MultiPolygon<Polygon<P>> =
455 MultiPolygon::from_vec(vec![unit_at(0.0, 0.0), unit_at(5.0, 0.0)]);
456 let got = ShoelaceMultiPolygonArea.area(&mpg);
457 assert!((got - 2.0).abs() < 1e-12);
458 }
459
460 /// Open ring of a 2x2 square (no repeated closing vertex): the
461 /// strategy must add the implicit last->first edge so the area
462 /// still comes out to 4.
463 #[test]
464 fn open_ring_2x2_square_is_4() {
465 let mut r = Ring::<P, true, false>::new();
466 r.push(Point2D::new(0.0, 0.0));
467 r.push(Point2D::new(0.0, 2.0));
468 r.push(Point2D::new(2.0, 2.0));
469 r.push(Point2D::new(2.0, 0.0));
470 let got = ShoelaceArea.area(&r);
471 assert!((got - 4.0).abs() < 1e-12);
472 }
473
474 /// Counter-clockwise declared ring traversed CCW (matching its
475 /// declared order) → positive area. Mirrors
476 /// `area.cpp:69-73` whose diamond on `polygon<P, false>` yields 2.
477 #[test]
478 fn ccw_declared_ccw_traversed_diamond_is_2() {
479 let r: Ring<P, false> = Ring::from_vec(vec![
480 Point2D::new(1.0, 0.0),
481 Point2D::new(0.0, 1.0),
482 Point2D::new(-1.0, 0.0),
483 Point2D::new(0.0, -1.0),
484 Point2D::new(1.0, 0.0),
485 ]);
486 let got = ShoelaceArea.area(&r);
487 assert!((got - 2.0).abs() < 1e-12);
488 }
489
490 // KC1.T2 witness: proves this strategy accepts a geometry whose
491 // `Point` is read-only (need not implement `PointMut`). If it
492 // compiles, the read-only bound is locked.
493 fn accepts_readonly_point<G, S>(s: &S, g: &G) -> S::Out
494 where
495 G: geometry_trait::Geometry,
496 <G as geometry_trait::Geometry>::Point: geometry_trait::Point,
497 S: AreaStrategy<G>,
498 {
499 s.area(g)
500 }
501}