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 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}