use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem, GeographicFamily, SphericalFamily};
use geometry_tag::SameAs;
use geometry_trait::{
Box, Closure, Geometry, MultiPolygon, Point, PointOrder, Polygon, Ring, corner,
};
pub trait AreaStrategy<G: Geometry> {
type Out: CoordinateScalar;
fn area(&self, g: &G) -> Self::Out;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ShoelaceArea;
#[derive(Debug, Default, Clone, Copy)]
pub struct ShoelacePolygonArea;
#[derive(Debug, Default, Clone, Copy)]
pub struct ShoelaceBoxArea;
#[derive(Debug, Default, Clone, Copy)]
pub struct ShoelaceMultiPolygonArea;
impl<R> AreaStrategy<R> for ShoelaceArea
where
R: Ring,
<R::Point as Point>::Cs: CoordinateSystem,
<<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Out = <R::Point as Point>::Scalar;
#[inline]
fn area(&self, r: &R) -> Self::Out {
let acc = shoelace_accumulator::<R>(r);
let two = <Self::Out as CoordinateScalar>::ONE + <Self::Out as CoordinateScalar>::ONE;
let half = acc / two;
match r.point_order() {
PointOrder::Clockwise => half,
PointOrder::CounterClockwise => -half,
}
}
}
impl<P> AreaStrategy<P> for ShoelacePolygonArea
where
P: Polygon,
ShoelaceArea: AreaStrategy<P::Ring, Out = <P::Point as Point>::Scalar>,
{
type Out = <P::Point as Point>::Scalar;
#[inline]
fn area(&self, p: &P) -> Self::Out {
let mut total = ShoelaceArea.area(p.exterior());
for inner in p.interiors() {
total = total + ShoelaceArea.area(inner);
}
total
}
}
impl<B> AreaStrategy<B> for ShoelaceBoxArea
where
B: Box,
<B::Point as Point>::Cs: CoordinateSystem,
<<B::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Out = <B::Point as Point>::Scalar;
#[inline]
fn area(&self, b: &B) -> Self::Out {
let xmin = b.get_indexed::<{ corner::MIN }, 0>();
let ymin = b.get_indexed::<{ corner::MIN }, 1>();
let xmax = b.get_indexed::<{ corner::MAX }, 0>();
let ymax = b.get_indexed::<{ corner::MAX }, 1>();
(xmax - xmin) * (ymax - ymin)
}
}
impl<MPg> AreaStrategy<MPg> for ShoelaceMultiPolygonArea
where
MPg: MultiPolygon,
ShoelacePolygonArea: AreaStrategy<MPg::ItemPolygon, Out = <MPg::Point as Point>::Scalar>,
{
type Out = <MPg::Point as Point>::Scalar;
#[inline]
fn area(&self, mpg: &MPg) -> Self::Out {
let mut total = <Self::Out as CoordinateScalar>::ZERO;
for p in mpg.polygons() {
total = total + ShoelacePolygonArea.area(p);
}
total
}
}
#[inline]
fn shoelace_accumulator<R>(r: &R) -> <R::Point as Point>::Scalar
where
R: Ring,
{
let mut acc = <<R::Point as Point>::Scalar as CoordinateScalar>::ZERO;
let it = r.points();
let next = it.clone().skip(1);
for (a, b) in it.zip(next) {
acc = acc + segment_term::<R::Point>(a, b);
}
if matches!(r.closure(), Closure::Open) {
let mut points = r.points();
if let Some(first) = points.next() {
let last = points.last().unwrap_or(first);
acc = acc + segment_term::<R::Point>(last, first);
}
}
acc
}
#[inline]
fn segment_term<P>(a: &P, b: &P) -> P::Scalar
where
P: Point,
{
(a.get::<0>() + b.get::<0>()) * (a.get::<1>() - b.get::<1>())
}
pub trait DefaultArea<Family> {
type Strategy: Default;
}
impl DefaultArea<CartesianFamily> for CartesianFamily {
type Strategy = ShoelacePolygonArea;
}
impl DefaultArea<SphericalFamily> for SphericalFamily {
type Strategy = crate::spherical::SphericalPolygonArea;
}
impl DefaultArea<GeographicFamily> for GeographicFamily {
type Strategy = crate::geographic::GeographicPolygonArea;
}
pub type DefaultAreaStrategy<G> =
<<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultArea<
<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
>>::Strategy;
#[cfg(test)]
mod tests {
use super::{
AreaStrategy, ShoelaceArea, ShoelaceBoxArea, ShoelaceMultiPolygonArea, ShoelacePolygonArea,
};
use geometry_cs::Cartesian;
use geometry_model::{Box, MultiPolygon, Point2D, Polygon, Ring, polygon};
type P = Point2D<f64, Cartesian>;
#[test]
fn ring_diamond_is_2() {
let r: Ring<P> = Ring::from_vec(vec![
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 2.0),
Point2D::new(3.0, 1.0),
Point2D::new(2.0, 0.0),
Point2D::new(1.0, 1.0),
]);
let got = accepts_readonly_point(&ShoelaceArea, &r);
assert!((got - 2.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),
]);
let got = ShoelaceArea.area(&r);
assert!((got - 16.0).abs() < 1e-12);
}
#[test]
fn ring_wrongly_ordered_is_minus_16() {
let r: Ring<P> = Ring::from_vec(vec![
Point2D::new(0.0, 0.0),
Point2D::new(2.0, 0.0),
Point2D::new(4.0, 2.0),
Point2D::new(0.0, 7.0),
Point2D::new(0.0, 0.0),
]);
let got = ShoelaceArea.area(&r);
assert!((got - -16.0).abs() < 1e-12);
}
#[test]
fn polygon_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)]];
let got = ShoelacePolygonArea.area(&p);
assert!((got - -1.0).abs() < 1e-12);
}
#[test]
fn polygon_pentagon_with_hole_is_15() {
let outer: 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),
]);
let hole: Ring<P> = Ring::from_vec(vec![
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 1.0),
Point2D::new(2.0, 2.0),
Point2D::new(1.0, 2.0),
Point2D::new(1.0, 1.0),
]);
let mut p: Polygon<P> = Polygon::new(outer);
p.inners.push(hole);
let got = ShoelacePolygonArea.area(&p);
assert!((got - 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::new(2.0, 2.0),
);
let got = ShoelaceBoxArea.area(&b);
assert!((got - 4.0).abs() < 1e-12);
}
#[test]
fn multipolygon_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)]);
let got = ShoelaceMultiPolygonArea.area(&mpg);
assert!((got - 2.0).abs() < 1e-12);
}
#[test]
fn open_ring_2x2_square_is_4() {
let mut r = Ring::<P, true, false>::new();
r.push(Point2D::new(0.0, 0.0));
r.push(Point2D::new(0.0, 2.0));
r.push(Point2D::new(2.0, 2.0));
r.push(Point2D::new(2.0, 0.0));
let got = ShoelaceArea.area(&r);
assert!((got - 4.0).abs() < 1e-12);
}
#[test]
fn ccw_declared_ccw_traversed_diamond_is_2() {
let r: Ring<P, false> = Ring::from_vec(vec![
Point2D::new(1.0, 0.0),
Point2D::new(0.0, 1.0),
Point2D::new(-1.0, 0.0),
Point2D::new(0.0, -1.0),
Point2D::new(1.0, 0.0),
]);
let got = ShoelaceArea.area(&r);
assert!((got - 2.0).abs() < 1e-12);
}
fn accepts_readonly_point<G, S>(s: &S, g: &G) -> S::Out
where
G: geometry_trait::Geometry,
<G as geometry_trait::Geometry>::Point: geometry_trait::Point,
S: AreaStrategy<G>,
{
s.area(g)
}
}