Skip to main content

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}