Skip to main content

geometry_algorithm/
area.rs

1//! `area(&g)` — see `boost/geometry/algorithms/area.hpp`.
2//!
3//! Cartesian-only in v1; spherical / geographic area strategies arrive
4//! alongside the Haversine / Andoyer / Vincenty work in later tasks.
5//!
6//! # Why four entry points
7//!
8//! Boost overloads on the same name `area` and resolves the right
9//! per-tag `dispatch::area` arm at the call site
10//! (`algorithms/area.hpp:149-187`). Rust has no overloading, and the
11//! Cartesian strategy in `geometry-strategy::area` is intentionally
12//! split into four sibling unit-structs (one per geometry kind) to
13//! sidestep coherence — see that module's docs. The split is
14//! reflected here as four entry points keyed on the input geometry's
15//! kind; the names follow the same `<kind>_area` shape that
16//! `length` / `perimeter` already use for the same reason
17//! (`crates/geometry-algorithm/src/length.rs`).
18
19use 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
26/// Shorthand for the CS family of `G`'s point type. Keeps the `where`
27/// clauses readable; matches the projection in
28/// [`geometry_strategy::DefaultAreaStrategy`].
29type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
30
31/// Signed area of a [`Ring`] via the Cartesian shoelace formula.
32///
33/// Mirrors `boost::geometry::area(ring)` from
34/// `boost/geometry/algorithms/area.hpp` resolved through the
35/// `dispatch::area<Ring, ring_tag>` arm at
36/// `algorithms/area.hpp:154-157`.
37///
38/// # Sign convention
39///
40/// Follows Boost: rings traversed in their declared
41/// [`geometry_trait::PointOrder`] produce a positive area,
42/// rings traversed in the opposite direction produce a negative area
43/// (`test/algorithms/area/area.cpp:63-64`).
44#[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/// Signed area of a [`Polygon`]: exterior-ring area plus the sum of
55/// interior-ring areas (interior rings are conventionally wound
56/// opposite the exterior, so their signed area already cancels).
57///
58/// Mirrors `boost::geometry::area(polygon)` from
59/// `boost/geometry/algorithms/area.hpp` resolved through the
60/// `dispatch::area<Polygon, polygon_tag>` arm at
61/// `algorithms/area.hpp:160-172`.
62///
63/// The area is computed with the default strategy for the polygon's
64/// coordinate-system family via
65/// [`geometry_strategy::DefaultAreaStrategy`] — Cartesian input
66/// resolves to the shoelace
67/// [`geometry_strategy::ShoelacePolygonArea`] (identical to the v1
68/// behaviour), spherical to the spherical-excess
69/// [`geometry_strategy::SphericalPolygonArea`], geographic to the
70/// authalic-sphere [`geometry_strategy::GeographicPolygonArea`]. For an
71/// explicit strategy use [`area_with`].
72///
73/// # Behaviour on "wrong" kinds
74///
75/// This *static* entry point requires a `Polygon`; a `Point` or
76/// `LineString` argument is a compile error. Boost's runtime "area of
77/// a non-areal kind is 0" contract is honoured on the *dynamic* path:
78/// [`crate::area_dyn`] returns `0` for `Point`,
79/// `LineString`, `MultiPoint`, … (the static/dynamic split is the
80/// same coherence workaround described on [`length`](fn@crate::length)).
81#[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/// Area of a polygon using an explicitly supplied strategy.
93///
94/// Mirrors the `area(g, strategy)` overload at
95/// `boost/geometry/algorithms/area.hpp`. Taking the strategy by value
96/// matches the by-value call shape of [`crate::distance_with`];
97/// concrete strategies are zero-sized or small `Copy` configuration
98/// objects, so this monomorphises into nothing.
99#[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/// Area of an axis-aligned [`Box`]: `(xmax - xmin) * (ymax - ymin)`.
114///
115/// Mirrors `boost::geometry::area(box)` from
116/// `boost/geometry/algorithms/area.hpp` resolved through the
117/// `dispatch::area<Box, box_tag>` arm at
118/// `algorithms/area.hpp:149-151`. Always non-negative — the Cartesian
119/// box formula is sign-blind to corner ordering
120/// (`test/algorithms/area/area.cpp:56-57`).
121#[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/// Signed area of a [`MultiPolygon`]: sum of the signed areas of its
132/// member polygons.
133///
134/// Mirrors `boost::geometry::area(multi_polygon)` from
135/// `boost/geometry/algorithms/area.hpp` resolved through the
136/// `dispatch::area<MultiGeometry, multi_polygon_tag>` arm at
137/// `algorithms/area.hpp:175-187`.
138#[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    //! Reference values from `geometry/test/algorithms/area/area.cpp`
151    //! (lines 45-64) and `area_sph_geo.cpp` / `area_geo.cpp` for the
152    //! spherical / geographic dispatch cases; see also the quickstart
153    //! `Area: 3.015` example.
154    #![allow(
155        clippy::float_cmp,
156        reason = "areas are compared with an explicit tolerance, not `==`"
157    )]
158
159    use super::{area, 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    /// `area.cpp:45` — rotated unit square diamond, area = 2.
166    #[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    /// `area.cpp:47` — pentagon, area = 16.
173    #[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    /// `area.cpp:48` — unit-square CCW on default-CW polygon → -1.
180    #[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    /// `area.cpp:49` — pentagon (16) with a unit-square hole (-1) = 15.
187    #[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    /// `area.cpp:56-57` — a 2x2 box has area 4, regardless of corner
197    /// order.
198    #[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    /// `area.cpp:63` — ring directly (no polygon wrapper), area = 16.
208    #[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    /// `doc/quickstart.qbk` — the canonical "Area: 3.015" example.
221    #[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    /// Multi-polygon: two disjoint CW unit squares → area = 2.
228    #[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    /// `area_sph_geo.cpp:93-106` — strategy-less `area` on a spherical
245    /// polygon resolves to `SphericalPolygonArea`; `POLYGON((0 0,0 90,
246    /// 90 0,0 0))` on the default (Earth) sphere covers `1/8` of it,
247    /// i.e. `4π/8 · R²`.
248    #[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        // Default SphericalPolygonArea uses R = 6_371_000 m.
265        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    /// `area_geo.cpp` — strategy-less `area` on a geographic polygon
274    /// resolves to the authalic-sphere `GeographicPolygonArea`; a
275    /// 1° × 1° box near the equator on WGS84 ≈ `12_309` km² (within 2 %).
276    #[cfg(feature = "std")]
277    #[test]
278    fn geographic_area_dispatches_to_authalic_sphere() {
279        use geometry_adapt::{Adapt, WithCs};
280        use geometry_cs::{Degree, Geographic};
281
282        type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
283        let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
284
285        let pg: Polygon<Gg> = Polygon::new(Ring::from_vec(vec![
286            gg(0., 0.),
287            gg(1., 0.),
288            gg(1., 1.),
289            gg(0., 1.),
290            gg(0., 0.),
291        ]));
292        let got = area(&pg).abs();
293        let expected = 12_309e6;
294        assert!(
295            (got - expected).abs() / expected < 0.02,
296            "got {} km² expected ~12309 km²",
297            got / 1e6
298        );
299    }
300}