#![allow(
clippy::similar_names,
reason = "The centroid accumulators `sum_x`/`sum_y` are the natural, domain-standard names for the per-axis running sums."
)]
use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_tag::{BoxTag, LinestringTag, MultiPointTag, PolygonTag, RingTag, SameAs, SegmentTag};
use geometry_trait::{
Box as BoxTrait, Geometry, Linestring as LinestringTrait, MultiPoint as MultiPointTrait,
Point as PointTrait, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
Segment as SegmentTrait, box_max, box_min, segment_end, segment_start,
};
use crate::area::{AreaStrategy, ShoelaceArea};
use crate::cartesian::Pythagoras;
use crate::distance::DistanceStrategy;
pub trait CentroidStrategy<G: Geometry> {
type Output: PointMut + Default;
fn centroid(&self, g: &G) -> Self::Output;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianRingCentroid;
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianPolygonCentroid;
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianLinestringCentroid;
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianSegmentCentroid;
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianBoxCentroid;
#[derive(Debug, Default, Clone, Copy)]
pub struct CartesianMultiPointCentroid;
#[inline]
fn point_2d<P>(x: P::Scalar, y: P::Scalar) -> P
where
P: PointTrait + PointMut + Default,
{
let mut p = P::default();
p.set::<0>(x);
p.set::<1>(y);
p
}
#[inline]
fn two<T: CoordinateScalar>() -> T {
T::ONE + T::ONE
}
#[inline]
fn three<T: CoordinateScalar>() -> T {
T::ONE + T::ONE + T::ONE
}
type BasheinDetmerSums<R> = (
<<R as Geometry>::Point as PointTrait>::Scalar,
<<R as Geometry>::Point as PointTrait>::Scalar,
<<R as Geometry>::Point as PointTrait>::Scalar,
);
fn bashein_detmer_sums<R>(r: &R) -> BasheinDetmerSums<R>
where
R: RingTrait,
R::Point: PointTrait,
{
let zero = <R::Point as PointTrait>::Scalar::ZERO;
let mut sum_a2 = zero;
let mut sum_x = zero;
let mut sum_y = zero;
let mut acc = |a: &R::Point, b: &R::Point| {
let x1 = a.get::<0>();
let y1 = a.get::<1>();
let x2 = b.get::<0>();
let y2 = b.get::<1>();
let ai = x1 * y2 - x2 * y1;
sum_a2 = sum_a2 + ai;
sum_x = sum_x + ai * (x1 + x2);
sum_y = sum_y + ai * (y1 + y2);
};
let it = r.points();
let next = it.clone().skip(1);
for (a, b) in it.zip(next) {
acc(a, b);
}
if matches!(r.closure(), geometry_trait::Closure::Open) {
let mut points = r.points();
if let Some(first) = points.next() {
let last = points.last().unwrap_or(first);
acc(last, first);
}
}
(sum_a2, sum_x, sum_y)
}
impl<G> CentroidStrategy<G> for CartesianRingCentroid
where
G: RingTrait,
G::Point: PointTrait + PointMut + Default + Copy,
<<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
ShoelaceArea: AreaStrategy<G, Out = <G::Point as PointTrait>::Scalar>,
{
type Output = G::Point;
fn centroid(&self, r: &G) -> G::Point {
let (sum_a2, sum_x, sum_y) = bashein_detmer_sums(r);
let zero = <G::Point as PointTrait>::Scalar::ZERO;
if sum_a2 == zero {
return r.points().next().copied().unwrap_or_default();
}
let a3 = three::<<G::Point as PointTrait>::Scalar>() * sum_a2;
point_2d::<G::Point>(sum_x / a3, sum_y / a3)
}
}
impl<G> CentroidStrategy<G> for CartesianPolygonCentroid
where
G: PolygonTrait,
G::Point: PointTrait + PointMut + Default + Copy,
<<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
ShoelaceArea: AreaStrategy<G::Ring, Out = <G::Point as PointTrait>::Scalar>,
CartesianRingCentroid: CentroidStrategy<G::Ring, Output = G::Point>,
{
type Output = G::Point;
fn centroid(&self, pg: &G) -> G::Point {
let zero = <G::Point as PointTrait>::Scalar::ZERO;
let mut sum_area = zero;
let mut sum_x = zero;
let mut sum_y = zero;
let mut fold_ring = |ring: &G::Ring| {
let area = ShoelaceArea.area(ring);
let c = CartesianRingCentroid.centroid(ring);
sum_area = sum_area + area;
sum_x = sum_x + area * c.get::<0>();
sum_y = sum_y + area * c.get::<1>();
};
fold_ring(pg.exterior());
for inner in pg.interiors() {
fold_ring(inner);
}
if sum_area == zero {
return pg.exterior().points().next().copied().unwrap_or_default();
}
point_2d::<G::Point>(sum_x / sum_area, sum_y / sum_area)
}
}
impl<G> CentroidStrategy<G> for CartesianLinestringCentroid
where
G: LinestringTrait,
G::Point: PointTrait + PointMut + Default + Copy,
<<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
Pythagoras: DistanceStrategy<G::Point, G::Point, Out = <G::Point as PointTrait>::Scalar>,
{
type Output = G::Point;
fn centroid(&self, ls: &G) -> G::Point {
let zero = <G::Point as PointTrait>::Scalar::ZERO;
let half =
<G::Point as PointTrait>::Scalar::ONE / two::<<G::Point as PointTrait>::Scalar>();
let mut total_len = zero;
let mut sum_x = zero;
let mut sum_y = zero;
let it = ls.points();
let next = it.clone().skip(1);
for (a, b) in it.zip(next) {
let seg_len = Pythagoras.distance(a, b);
let mid_x = (a.get::<0>() + b.get::<0>()) * half;
let mid_y = (a.get::<1>() + b.get::<1>()) * half;
total_len = total_len + seg_len;
sum_x = sum_x + seg_len * mid_x;
sum_y = sum_y + seg_len * mid_y;
}
if total_len == zero {
return ls.points().next().copied().unwrap_or_default();
}
point_2d::<G::Point>(sum_x / total_len, sum_y / total_len)
}
}
impl<G> CentroidStrategy<G> for CartesianSegmentCentroid
where
G: SegmentTrait,
G::Point: PointTrait + PointMut + Default + Copy,
<<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Output = G::Point;
fn centroid(&self, s: &G) -> G::Point {
let a = segment_start(s);
let b = segment_end(s);
midpoint(&a, &b)
}
}
impl<G> CentroidStrategy<G> for CartesianBoxCentroid
where
G: BoxTrait,
G::Point: PointTrait + PointMut + Default + Copy,
<<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Output = G::Point;
fn centroid(&self, b: &G) -> G::Point {
let lo = box_min(b);
let hi = box_max(b);
midpoint(&lo, &hi)
}
}
impl<G> CentroidStrategy<G> for CartesianMultiPointCentroid
where
G: MultiPointTrait,
G::ItemPoint: PointTrait + PointMut + Default + Copy,
<<G::ItemPoint as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type Output = G::ItemPoint;
fn centroid(&self, mp: &G) -> G::ItemPoint {
let zero = <G::ItemPoint as PointTrait>::Scalar::ZERO;
let mut count = zero;
let mut sum_x = zero;
let mut sum_y = zero;
for p in mp.points() {
sum_x = sum_x + p.get::<0>();
sum_y = sum_y + p.get::<1>();
count = count + <G::ItemPoint as PointTrait>::Scalar::ONE;
}
if count == zero {
return G::ItemPoint::default();
}
point_2d::<G::ItemPoint>(sum_x / count, sum_y / count)
}
}
#[inline]
fn midpoint<P>(a: &P, b: &P) -> P
where
P: PointTrait + PointMut + Default,
{
let half = P::Scalar::ONE / two::<P::Scalar>();
let x = (a.get::<0>() + b.get::<0>()) * half;
let y = (a.get::<1>() + b.get::<1>()) * half;
point_2d::<P>(x, y)
}
#[doc(hidden)]
pub trait CentroidStrategyForKind {
type S: Default;
}
impl CentroidStrategyForKind for RingTag {
type S = CartesianRingCentroid;
}
impl CentroidStrategyForKind for PolygonTag {
type S = CartesianPolygonCentroid;
}
impl CentroidStrategyForKind for LinestringTag {
type S = CartesianLinestringCentroid;
}
impl CentroidStrategyForKind for SegmentTag {
type S = CartesianSegmentCentroid;
}
impl CentroidStrategyForKind for BoxTag {
type S = CartesianBoxCentroid;
}
impl CentroidStrategyForKind for MultiPointTag {
type S = CartesianMultiPointCentroid;
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
reason = "centroids are compared with an explicit absolute tolerance, not `==`"
)]
use super::{
CartesianBoxCentroid, CartesianLinestringCentroid, CartesianMultiPointCentroid,
CartesianPolygonCentroid, CartesianRingCentroid, CartesianSegmentCentroid,
CentroidStrategy,
};
use geometry_cs::Cartesian;
use geometry_model::{Box, MultiPoint, Point2D, Polygon, Ring, Segment, linestring, polygon};
use geometry_trait::Point as _;
type Pt = Point2D<f64, Cartesian>;
fn close_pt(got: &Pt, x: f64, y: f64, tol: f64) -> bool {
(got.get::<0>() - x).abs() < tol && (got.get::<1>() - y).abs() < tol
}
#[test]
fn ring_centroid_unit_square_shift() {
let r: Ring<Pt> = Ring::from_vec(vec![
Pt::new(1., 1.),
Pt::new(1., 2.),
Pt::new(2., 2.),
Pt::new(2., 1.),
Pt::new(1., 1.),
]);
let c = CartesianRingCentroid.centroid(&r);
assert!(close_pt(&c, 1.5, 1.5, 1e-9));
}
#[test]
fn ring_bashein_detmer_reference() {
let r: Ring<Pt> = Ring::from_vec(vec![
Pt::new(2., 1.3),
Pt::new(2.4, 1.7),
Pt::new(2.8, 1.8),
Pt::new(3.4, 1.2),
Pt::new(3.7, 1.6),
Pt::new(3.4, 2.),
Pt::new(4.1, 3.),
Pt::new(5.3, 2.6),
Pt::new(5.4, 1.2),
Pt::new(4.9, 0.8),
Pt::new(2.9, 0.7),
Pt::new(2., 1.3),
]);
let c = CartesianRingCentroid.centroid(&r);
assert!(close_pt(
&c,
4.069_233_630_952_38,
1.650_558_035_714_29,
1e-9
));
}
#[test]
fn polygon_10x10_square_centroid_is_5_5() {
let pg: Polygon<Pt> = polygon![[(0., 0.), (0., 10.), (10., 10.), (10., 0.), (0., 0.)]];
let c = CartesianPolygonCentroid.centroid(&pg);
assert!(close_pt(&c, 5.0, 5.0, 1e-9));
}
#[test]
fn polygon_unit_square_centroid_is_half_half() {
let pg: Polygon<Pt> = polygon![[(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)]];
let c = CartesianPolygonCentroid.centroid(&pg);
assert!(close_pt(&c, 0.5, 0.5, 1e-9));
}
#[test]
fn polygon_with_hole_reference() {
let pg: Polygon<Pt> = polygon![
[
(2., 1.3),
(2.4, 1.7),
(2.8, 1.8),
(3.4, 1.2),
(3.7, 1.6),
(3.4, 2.),
(4.1, 3.),
(5.3, 2.6),
(5.4, 1.2),
(4.9, 0.8),
(2.9, 0.7),
(2., 1.3)
],
[(4., 2.), (4.2, 1.4), (4.8, 1.9), (4.4, 2.2), (4., 2.)]
];
let c = CartesianPolygonCentroid.centroid(&pg);
assert!(close_pt(
&c,
4.046_626_506_024_1,
1.634_899_598_393_57,
1e-9
));
}
#[test]
fn degenerate_zero_area_polygon_returns_first_vertex() {
let pg: Polygon<Pt> = polygon![[
(1., 1.),
(4., -2.),
(4., 2.),
(10., 0.),
(1., 0.),
(10., 1.),
(1., 1.)
]];
let c = CartesianPolygonCentroid.centroid(&pg);
assert!(close_pt(&c, 1.0, 1.0, 1e-9));
}
#[test]
fn linestring_centroid_diagonal() {
let ls = linestring![(1., 1.), (2., 2.), (3., 3.)];
let c = CartesianLinestringCentroid.centroid(&ls);
assert!(close_pt(&c, 2.0, 2.0, 1e-9));
}
#[test]
fn linestring_centroid_bent() {
let ls = linestring![(0., 0.), (0., 4.), (4., 4.)];
let c = CartesianLinestringCentroid.centroid(&ls);
assert!(close_pt(&c, 1.0, 3.0, 1e-9));
}
#[test]
fn linestring_degenerate_returns_first_point() {
let ls = linestring![(1., 1.), (1., 1.)];
let c = CartesianLinestringCentroid.centroid(&ls);
assert!(close_pt(&c, 1.0, 1.0, 1e-9));
}
#[test]
fn segment_midpoint() {
let s = Segment::new(Pt::new(1., 1.), Pt::new(3., 3.));
let c = CartesianSegmentCentroid.centroid(&s);
assert!(close_pt(&c, 2.0, 2.0, 1e-12));
}
#[test]
fn box_centroid() {
let b: Box<Pt> = Box::from_corners(Pt::new(1., 2.), Pt::new(3., 4.));
let c = CartesianBoxCentroid.centroid(&b);
assert!(close_pt(&c, 2.0, 3.0, 1e-12));
}
#[test]
fn multipoint_mean() {
let mp: MultiPoint<Pt> =
MultiPoint::from_vec(vec![Pt::new(0., 0.), Pt::new(2., 0.), Pt::new(0., 2.)]);
let c = CartesianMultiPointCentroid.centroid(&mp);
assert!(close_pt(&c, 2.0 / 3.0, 2.0 / 3.0, 1e-9));
}
}