Skip to main content

geometry_algorithm/
centroid.rs

1//! `centroid(&g)` — the geometric centre of `g`.
2//!
3//! Mirrors `boost::geometry::centroid(g, p)` from
4//! `boost/geometry/algorithms/centroid.hpp` — except Boost returns
5//! through an out-parameter while Rust returns the point by value. Per
6//! geometry kind (all Cartesian in LA6; spherical / geographic siblings
7//! land in LA8):
8//!
9//! * `Polygon`    → [`geometry_strategy::CartesianPolygonCentroid`]
10//! * `Ring`       → [`geometry_strategy::CartesianRingCentroid`]
11//! * `Linestring` → [`geometry_strategy::CartesianLinestringCentroid`]
12//! * `Segment`    → [`geometry_strategy::CartesianSegmentCentroid`]
13//! * `Box`        → [`geometry_strategy::CartesianBoxCentroid`]
14//! * `MultiPoint` → [`geometry_strategy::CartesianMultiPointCentroid`]
15//!
16//! The per-kind strategy is selected type-level via
17//! [`geometry_strategy::CentroidStrategyForKind`], a tag-keyed picker
18//! that routes `G::Kind` to the right per-kind strategy struct. Because
19//! it keys on the tag, any concept-adapted foreign type resolves through
20//! the same path as the equivalent `geometry-model` value.
21//! `centroid_with` takes an explicit
22//! strategy.
23//!
24//! # Spherical / geographic centroid — deferred (LA8.T3)
25//!
26//! Unlike `length` / `area` / `azimuth` — whose LA8 free functions
27//! dispatch by coordinate-system family — `centroid` stays Cartesian.
28//! Boost ships no validated spherical / geographic centroid reference
29//! values, so shipping that math would be unverifiable; see the
30//! deferral note on [`geometry_strategy::CentroidStrategyForKind`]. A
31//! spherical or geographic geometry is therefore a compile error here;
32//! use [`centroid_with`] with an explicit strategy in the meantime.
33
34use geometry_strategy::{CentroidStrategy, CentroidStrategyForKind};
35use geometry_trait::Geometry;
36
37/// Centroid of `g` using the per-kind Cartesian strategy.
38///
39/// Mirrors `boost::geometry::centroid(g, p)` from
40/// `boost/geometry/algorithms/centroid.hpp`. The strategy is chosen from
41/// the geometry *kind* via the tag-keyed
42/// [`geometry_strategy::CentroidStrategyForKind`] picker; for an
43/// explicit strategy (or a future spherical / geographic LA8 strategy)
44/// use [`centroid_with`].
45#[inline]
46#[must_use]
47pub fn centroid<G>(
48    g: &G,
49) -> <<G::Kind as CentroidStrategyForKind>::S as CentroidStrategy<G>>::Output
50where
51    G: Geometry,
52    G::Kind: CentroidStrategyForKind,
53    <G::Kind as CentroidStrategyForKind>::S: CentroidStrategy<G>,
54{
55    <<G::Kind as CentroidStrategyForKind>::S as Default>::default().centroid(g)
56}
57
58/// Centroid of `g` using an explicitly supplied strategy.
59///
60/// Mirrors the `centroid(g, p, strategy)` overload at
61/// `boost/geometry/algorithms/centroid.hpp`. Taking the strategy by
62/// value (rather than by reference) matches the by-value call shape of
63/// [`crate::distance_with`]; concrete strategies are
64/// zero-sized configuration objects, so this monomorphises into nothing.
65#[inline]
66#[must_use]
67#[allow(
68    clippy::needless_pass_by_value,
69    reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
70)]
71pub fn centroid_with<G, S>(g: &G, s: S) -> S::Output
72where
73    G: Geometry,
74    S: CentroidStrategy<G>,
75{
76    s.centroid(g)
77}
78
79#[cfg(test)]
80mod tests {
81    //! Reference values from `geometry/test/algorithms/centroid.cpp` —
82    //! the same fixtures the LA6.T1 strategy tests use, reached here
83    //! through the free function.
84    #![allow(
85        clippy::float_cmp,
86        reason = "centroids are compared with an explicit absolute tolerance, not `==`"
87    )]
88
89    use super::centroid;
90    use geometry_cs::Cartesian;
91    use geometry_model::{Box, MultiPoint, Point2D, Polygon, Ring, Segment, linestring, polygon};
92    use geometry_trait::Point as _;
93
94    type Pt = Point2D<f64, Cartesian>;
95
96    fn close(got: Pt, x: f64, y: f64, tol: f64) -> bool {
97        (got.get::<0>() - x).abs() < tol && (got.get::<1>() - y).abs() < tol
98    }
99
100    // centroid.cpp:46 — POLYGON((0 0,0 10,10 10,10 0,0 0)) → (5, 5)
101    #[test]
102    fn polygon_10x10_square() {
103        let pg: Polygon<Pt> = polygon![[(0., 0.), (0., 10.), (10., 10.), (10., 0.), (0., 0.)]];
104        let c = centroid(&pg);
105        assert!(close(c, 5.0, 5.0, 1e-9));
106    }
107
108    // centroid.cpp:139 — ring POLYGON((1 1, 1 2, 2 2, 2 1, 1 1)) → (1.5, 1.5)
109    #[test]
110    fn ring_unit_square_shift() {
111        let r: Ring<Pt> = Ring::from_vec(vec![
112            Pt::new(1., 1.),
113            Pt::new(1., 2.),
114            Pt::new(2., 2.),
115            Pt::new(2., 1.),
116            Pt::new(1., 1.),
117        ]);
118        let c = centroid(&r);
119        assert!(close(c, 1.5, 1.5, 1e-9));
120    }
121
122    // centroid.cpp:73 — LINESTRING(1 1, 2 2, 3 3) → (2, 2)
123    #[test]
124    fn linestring_diagonal() {
125        let ls = linestring![(1., 1.), (2., 2.), (3., 3.)];
126        let c = centroid(&ls);
127        assert!(close(c, 2.0, 2.0, 1e-9));
128    }
129
130    // centroid.cpp:109 — segment (1 1) → (3 3) → (2, 2)
131    #[test]
132    fn segment_midpoint() {
133        let s = Segment::new(Pt::new(1., 1.), Pt::new(3., 3.));
134        let c = centroid(&s);
135        assert!(close(c, 2.0, 2.0, 1e-12));
136    }
137
138    // centroid.cpp:131 — box "POLYGON((1 2,3 4))" → (2, 3)
139    #[test]
140    fn box_centroid() {
141        let b: Box<Pt> = Box::from_corners(Pt::new(1., 2.), Pt::new(3., 4.));
142        let c = centroid(&b);
143        assert!(close(c, 2.0, 3.0, 1e-12));
144    }
145
146    // MultiPoint {(0,0),(2,0),(0,2)} → (2/3, 2/3)
147    #[test]
148    fn multipoint_mean() {
149        let mp: MultiPoint<Pt> =
150            MultiPoint::from_vec(vec![Pt::new(0., 0.), Pt::new(2., 0.), Pt::new(0., 2.)]);
151        let c = centroid(&mp);
152        assert!(close(c, 2.0 / 3.0, 2.0 / 3.0, 1e-9));
153    }
154}