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, area_with, 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 spherical_polygon_with_hole_subtracts_inner_area() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
use geometry_strategy::SphericalPolygonArea;
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
let outer = Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
let hole = Ring::from_vec(vec![sp(10., 10.), sp(30., 10.), sp(20., 20.), sp(10., 10.)]);
let pg: Polygon<Sp> = Polygon::with_inners(outer, vec![hole]);
let with_hole = area_with(&pg, SphericalPolygonArea::UNIT);
let whole = core::f64::consts::FRAC_PI_2;
assert!(with_hole < whole, "hole must reduce area: {with_hole}");
assert!(with_hole > 0.0, "still positive: {with_hole}");
}
#[cfg(feature = "std")]
#[test]
fn spherical_open_ring_closes_implicitly() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
use geometry_strategy::SphericalArea;
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
let closed: Ring<Sp> =
Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.), sp(0., 0.)]);
let open: Ring<Sp, true, false> =
Ring::from_vec(vec![sp(0., 0.), sp(0., 90.), sp(90., 0.)]);
let a_closed = area_with(&closed, SphericalArea::UNIT);
let a_open = area_with(&open, SphericalArea::UNIT);
assert!((a_closed - a_open).abs() < 1e-9, "{a_closed} vs {a_open}");
}
#[cfg(feature = "std")]
#[test]
fn spherical_antimeridian_crossing_normalises_dlon() {
use geometry_adapt::{Adapt, WithCs};
use geometry_cs::{Degree, Spherical};
use geometry_strategy::SphericalArea;
type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
let sp = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
let r: Ring<Sp> = Ring::from_vec(vec![
sp(170., 0.),
sp(170., 10.),
sp(-170., 10.),
sp(-170., 0.),
sp(170., 0.),
]);
let got = area_with(&r, SphericalArea::UNIT).abs();
assert!(got < 0.2, "expected a small sliver area, got {got}");
}
#[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);
}
#[cfg(feature = "std")]
#[test]
fn geographic_polygon_with_hole_subtracts_hole_area() {
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 outer = Ring::from_vec(vec![
gg(0., 0.),
gg(1., 0.),
gg(1., 1.),
gg(0., 1.),
gg(0., 0.),
]);
let hole: Ring<Gg> = Ring::from_vec(vec![
gg(0.2, 0.2),
gg(0.2, 0.8),
gg(0.8, 0.8),
gg(0.8, 0.2),
gg(0.2, 0.2),
]);
let outer_only = area(&Polygon::new(outer.clone())).abs();
let hole_alone = area(&Polygon::new(hole.clone())).abs();
let holed = Polygon::with_inners(outer, vec![hole]);
let got = area(&holed).abs();
let expected = outer_only - hole_alone;
assert!(
(got - expected).abs() / expected < 1e-9,
"got {got}, expected outer − hole = {expected}"
);
}
#[cfg(feature = "std")]
#[test]
fn geographic_open_ring_closes_implicitly() {
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 open: Polygon<Gg, true, false> = Polygon::new(Ring::from_vec(vec![
gg(0., 0.),
gg(1., 0.),
gg(1., 1.),
gg(0., 1.),
]));
let closed: 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_open = area(&open).abs();
let got_closed = area(&closed).abs();
assert!(
(got_open - got_closed).abs() / got_closed < 1e-12,
"open {got_open} != closed {got_closed}"
);
}
#[cfg(feature = "std")]
#[test]
fn geographic_ccw_declared_polygon_negates_sign() {
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 verts = vec![gg(0., 0.), gg(1., 0.), gg(1., 1.), gg(0., 1.), gg(0., 0.)];
let cw: Polygon<Gg, true> = Polygon::new(Ring::from_vec(verts.clone()));
let ccw: Polygon<Gg, false> = Polygon::new(Ring::from_vec(verts));
let sum = area(&cw) + area(&ccw);
assert!(
sum.abs() < 1e-3,
"signed areas do not cancel: cw + ccw = {sum}"
);
assert!(area(&cw).abs() > 1e9, "area unexpectedly tiny");
}
}