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}