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}