use geometry_cs::CoordinateSystem;
use geometry_strategy::{
AreaStrategy, DefaultArea, DefaultAreaStrategy, ShoelaceArea, ShoelaceBoxArea,
ShoelaceMultiPolygonArea,
};
use geometry_trait::{Box, Geometry, MultiPolygon, Point, Polygon, Ring};
type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
#[inline]
#[must_use]
pub fn ring_area<R>(r: &R) -> <ShoelaceArea as AreaStrategy<R>>::Out
where
R: Ring,
ShoelaceArea: AreaStrategy<R>,
{
ShoelaceArea.area(r)
}
#[inline]
#[must_use]
pub fn area<P>(p: &P) -> <DefaultAreaStrategy<P> as AreaStrategy<P>>::Out
where
P: Polygon,
Family<P>: DefaultArea<Family<P>>,
DefaultAreaStrategy<P>: AreaStrategy<P> + Default,
{
DefaultAreaStrategy::<P>::default().area(p)
}
#[inline]
#[must_use]
#[allow(
clippy::needless_pass_by_value,
reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
)]
pub fn area_with<G, S>(g: &G, s: S) -> S::Out
where
G: Geometry,
S: AreaStrategy<G>,
{
s.area(g)
}
#[inline]
#[must_use]
pub fn box_area<B>(b: &B) -> <ShoelaceBoxArea as AreaStrategy<B>>::Out
where
B: Box,
ShoelaceBoxArea: AreaStrategy<B>,
{
ShoelaceBoxArea.area(b)
}
#[inline]
#[must_use]
pub fn multi_polygon_area<MPg>(mpg: &MPg) -> <ShoelaceMultiPolygonArea as AreaStrategy<MPg>>::Out
where
MPg: MultiPolygon,
ShoelaceMultiPolygonArea: AreaStrategy<MPg>,
{
ShoelaceMultiPolygonArea.area(mpg)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
reason = "areas are compared with an explicit tolerance, not `==`"
)]
use super::{area, box_area, multi_polygon_area, ring_area};
use geometry_cs::Cartesian;
use geometry_model::{Box, MultiPolygon, Point2D, Polygon, Ring, polygon};
type P = Point2D<f64, Cartesian>;
#[test]
fn diamond_polygon_is_2() {
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)]];
assert!((area(&p) - 2.0).abs() < 1e-12);
}
#[test]
fn pentagon_is_16() {
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)]];
assert!((area(&p) - 16.0).abs() < 1e-12);
}
#[test]
fn ccw_unit_square_is_minus_1() {
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)]];
assert!((area(&p) - -1.0).abs() < 1e-12);
}
#[test]
fn pentagon_with_hole_is_15() {
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)],
[(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)],
];
assert!((area(&p) - 15.0).abs() < 1e-12);
}
#[test]
fn box_2x2_is_4() {
let b = Box::from_corners(
Point2D::<f64, Cartesian>::new(0.0, 0.0),
Point2D::<f64, Cartesian>::new(2.0, 2.0),
);
assert!((box_area(&b) - 4.0).abs() < 1e-12);
}
#[test]
fn ring_pentagon_is_16() {
let r: Ring<P> = Ring::from_vec(vec![
Point2D::new(0.0, 0.0),
Point2D::new(0.0, 7.0),
Point2D::new(4.0, 2.0),
Point2D::new(2.0, 0.0),
Point2D::new(0.0, 0.0),
]);
assert!((ring_area(&r) - 16.0).abs() < 1e-12);
}
#[test]
fn quickstart_polygon_area_is_3_015() {
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)]];
assert!((area(&p) - 3.015).abs() < 1e-3);
}
#[test]
fn multi_polygon_two_unit_squares_is_2() {
let unit_at = |x: f64, y: f64| -> Polygon<P> {
polygon![[
(x, y),
(x, y + 1.0),
(x + 1.0, y + 1.0),
(x + 1.0, y),
(x, y)
]]
};
let mpg: MultiPolygon<Polygon<P>> =
MultiPolygon::from_vec(vec![unit_at(0.0, 0.0), unit_at(5.0, 0.0)]);
assert!((multi_polygon_area(&mpg) - 2.0).abs() < 1e-12);
}
#[cfg(feature = "std")]
#[test]
fn spherical_area_dispatches_to_spherical_excess() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
let pg: Polygon<Sp> = Polygon::new(Ring::from_vec(vec![
sp(0., 0.),
sp(0., 90.),
sp(90., 0.),
sp(0., 0.),
]));
let got = area(&pg);
let r = 6_371_000.0_f64;
let expected = core::f64::consts::FRAC_PI_2 * r * r;
assert!(
(got - expected).abs() / expected < 1e-6,
"got {got} expected {expected}"
);
}
#[cfg(feature = "std")]
#[test]
fn geographic_area_dispatches_to_authalic_sphere() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Geographic};
type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
let gg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
let pg: Polygon<Gg> = Polygon::new(Ring::from_vec(vec![
gg(0., 0.),
gg(1., 0.),
gg(1., 1.),
gg(0., 1.),
gg(0., 0.),
]));
let got = area(&pg).abs();
let expected = 12_309e6;
assert!(
(got - expected).abs() / expected < 0.02,
"got {} km² expected ~12309 km²",
got / 1e6
);
}
}