geometry_strategy/centroid.rs
1//! `CentroidStrategy<G>` — geometric centre of a geometry.
2//!
3//! Mirrors the per-CS centroid-strategy concept from
4//! `boost/geometry/strategies/centroid/services.hpp` plus the Cartesian
5//! implementations in `boost/geometry/strategies/cartesian/centroid_*.hpp`
6//! and the per-kind dispatch in
7//! `boost/geometry/algorithms/centroid.hpp`. Per-kind Cartesian formulas:
8//!
9//! * `Segment`, `Box` → midpoint of endpoints / corners
10//! * `Linestring` → length-weighted midpoint of segments
11//! * `Ring` (closed) / `Polygon` → area-weighted Bashein–Detmer formula
12//! * `MultiPoint` → arithmetic mean of points
13//!
14//! Each per-kind impl lives behind a different strategy unit-struct so
15//! coherence stays disjoint — the same distinct-struct-per-kind trick as
16//! `area` (see `strategies/cartesian/area.hpp` and the module docs of
17//! [`crate::area`]). Rust cannot prove a single type is not both a
18//! `Ring` and a `Polygon`, so a single strategy carrying overlapping
19//! `impl CentroidStrategy<G>` blocks keyed off the open traits would be
20//! rejected (E0119); the sibling unit-structs below each carry a single
21//! concept-bounded impl (`impl<G: Ring> … for CartesianRingCentroid`, …)
22//! — distinct `Self`, so no overlap. The
23//! [`CentroidStrategyForKind`] picker then routes `G::Kind` (the tag
24//! [`Geometry::Kind`] already carries) to the right struct, disjoint on
25//! the tag. This opens every kind to any concept-adapted foreign type,
26//! not just the `geometry-model` structs.
27#![allow(
28 clippy::similar_names,
29 reason = "The centroid accumulators `sum_x`/`sum_y` are the natural, domain-standard names for the per-axis running sums."
30)]
31
32use geometry_coords::CoordinateScalar;
33use geometry_cs::{CartesianFamily, CoordinateSystem};
34use geometry_tag::{
35 BoxTag, LinestringTag, MultiPointTag, MultiPolygonTag, PolygonTag, RingTag, SameAs, SegmentTag,
36};
37use geometry_trait::{
38 Box as BoxTrait, Geometry, Linestring as LinestringTrait, MultiPoint as MultiPointTrait,
39 MultiPolygon as MultiPolygonTrait, Point as PointTrait, PointMut, Polygon as PolygonTrait,
40 Ring as RingTrait, Segment as SegmentTrait, box_max, box_min, segment_end, segment_start,
41};
42
43use crate::area::{AreaStrategy, ShoelaceArea};
44use crate::cartesian::Pythagoras;
45use crate::distance::DistanceStrategy;
46
47/// A strategy for computing the centroid of `G`.
48///
49/// Mirrors the per-CS centroid-strategy concept from
50/// `boost/geometry/strategies/centroid/services.hpp`. The Boost concept
51/// exposes a stateful `apply(p1, p2, state)` accumulator plus a
52/// `result(state)` reduction (see
53/// `strategies/cartesian/centroid_bashein_detmer.hpp:173-231`); the Rust
54/// analogue collapses the two phases into a single method
55/// [`CentroidStrategy::centroid`] keyed on the geometry type.
56pub trait CentroidStrategy<G: Geometry> {
57 /// The output point type. Almost always `G::Point` — Boost picks the
58 /// input point type by default
59 /// (`strategies/default_centroid_result.hpp`).
60 type Output: PointMut + Default;
61
62 /// Compute the centroid of `g`.
63 fn centroid(&self, g: &G) -> Self::Output;
64}
65
66/// Cartesian centroid for a [`geometry_trait::Ring`] — the Bashein–Detmer formula
67/// (signed-area-weighted vertex pairs).
68///
69/// Mirrors `boost::geometry::strategy::centroid::bashein_detmer` from
70/// `strategies/cartesian/centroid_bashein_detmer.hpp:173-231`, reached
71/// through the `areal_tag` arm of
72/// `boost/geometry/algorithms/centroid.hpp`.
73#[derive(Debug, Default, Clone, Copy)]
74pub struct CartesianRingCentroid;
75
76/// Cartesian centroid for a [`geometry_trait::Polygon`] — the [`CartesianRingCentroid`]
77/// formula applied to every ring (exterior plus interiors), combined by
78/// signed area.
79///
80/// Mirrors the polygon arm of
81/// `boost/geometry/algorithms/centroid.hpp`: each interior ring's
82/// (oppositely-wound, hence oppositely-signed) area-weighted centroid is
83/// folded into the running sum, so a plain area-weighted combine already
84/// performs the hole correction.
85#[derive(Debug, Default, Clone, Copy)]
86pub struct CartesianPolygonCentroid;
87
88/// Cartesian centroid for a [`geometry_trait::MultiPolygon`] — one
89/// Bashein–Detmer accumulator over every ring of every member.
90///
91/// Mirrors the multi-polygon arm of
92/// `boost/geometry/algorithms/centroid.hpp`.
93#[derive(Debug, Default, Clone, Copy)]
94pub struct CartesianMultiPolygonCentroid;
95
96/// Cartesian centroid for a [`geometry_trait::Linestring`] — length-weighted midpoint of
97/// each segment, summed and divided by total length.
98///
99/// Mirrors the `linear_tag` arm of
100/// `boost/geometry/algorithms/centroid.hpp` together with
101/// `strategies/cartesian/centroid_average.hpp`, which averages segment
102/// midpoints weighted by segment length.
103#[derive(Debug, Default, Clone, Copy)]
104pub struct CartesianLinestringCentroid;
105
106/// Cartesian centroid for a [`geometry_trait::Segment`] — `(start + end) / 2`.
107///
108/// Mirrors the `segment_tag` arm of
109/// `boost/geometry/algorithms/centroid.hpp`, which returns the segment
110/// midpoint.
111#[derive(Debug, Default, Clone, Copy)]
112pub struct CartesianSegmentCentroid;
113
114/// Cartesian centroid for a [`geometry_trait::Box`] — corner midpoint per dimension.
115///
116/// Mirrors the `box_tag` arm of
117/// `boost/geometry/algorithms/centroid.hpp`
118/// (`detail::centroid::centroid_box`), which returns the midpoint of the
119/// min / max corners.
120#[derive(Debug, Default, Clone, Copy)]
121pub struct CartesianBoxCentroid;
122
123/// Cartesian centroid for a [`geometry_trait::MultiPoint`] — arithmetic mean of the
124/// member points.
125///
126/// Mirrors the `pointlike_tag` arm of
127/// `boost/geometry/algorithms/centroid.hpp` together with
128/// `strategies/cartesian/centroid_average.hpp`.
129#[derive(Debug, Default, Clone, Copy)]
130pub struct CartesianMultiPointCentroid;
131
132// ---- helpers ---------------------------------------------------------
133
134/// Build a 2-D point from its two coordinates via [`Default`] +
135/// `set::<0>` / `set::<1>`. Shared by the areal (Bashein–Detmer) impls,
136/// which are inherently 2-D — the C++ strategy reads only `get<0>` /
137/// `get<1>` (`centroid_bashein_detmer.hpp:191-199`).
138#[inline]
139fn point_2d<P>(x: P::Scalar, y: P::Scalar) -> P
140where
141 P: PointTrait + PointMut + Default,
142{
143 let mut p = P::default();
144 p.set::<0>(x);
145 p.set::<1>(y);
146 p
147}
148
149/// The scalar `2` (`ONE + ONE`) for the argument scalar type.
150#[inline]
151fn two<T: CoordinateScalar>() -> T {
152 T::ONE + T::ONE
153}
154
155/// The scalar `3` for the argument scalar type — the `3 * sum_a2 = 6A`
156/// divisor of `centroid_bashein_detmer.hpp:211-212`.
157#[inline]
158fn three<T: CoordinateScalar>() -> T {
159 T::ONE + T::ONE + T::ONE
160}
161
162/// The Bashein–Detmer accumulator triple `(sum_a2, sum_x, sum_y)`, all in
163/// the ring's scalar type.
164type BasheinDetmerSums<R> = (
165 <<R as Geometry>::Point as PointTrait>::Scalar,
166 <<R as Geometry>::Point as PointTrait>::Scalar,
167 <<R as Geometry>::Point as PointTrait>::Scalar,
168);
169
170/// Sum the Bashein–Detmer accumulators `(sum_a2, sum_x, sum_y)` over the
171/// consecutive vertex pairs of `r`. Mirrors the per-segment `apply` at
172/// `centroid_bashein_detmer.hpp:191-199`:
173///
174/// ```text
175/// ai = x1 * y2 - x2 * y1
176/// sum_a2 += ai
177/// sum_x += ai * (x1 + x2)
178/// sum_y += ai * (y1 + y2)
179/// ```
180///
181/// For an open ring the implicit `last -> first` closing pair is added
182/// explicitly, mirroring the way [`crate::area`] closes an open ring.
183fn bashein_detmer_sums<R>(r: &R) -> BasheinDetmerSums<R>
184where
185 R: RingTrait,
186 R::Point: PointTrait,
187{
188 let zero = <R::Point as PointTrait>::Scalar::ZERO;
189 let mut sum_a2 = zero;
190 let mut sum_x = zero;
191 let mut sum_y = zero;
192
193 let mut acc = |a: &R::Point, b: &R::Point| {
194 let x1 = a.get::<0>();
195 let y1 = a.get::<1>();
196 let x2 = b.get::<0>();
197 let y2 = b.get::<1>();
198 let ai = x1 * y2 - x2 * y1;
199 sum_a2 = sum_a2 + ai;
200 sum_x = sum_x + ai * (x1 + x2);
201 sum_y = sum_y + ai * (y1 + y2);
202 };
203
204 let it = r.points();
205 let next = it.clone().skip(1);
206 for (a, b) in it.zip(next) {
207 acc(a, b);
208 }
209 if matches!(r.closure(), geometry_trait::Closure::Open) {
210 let mut points = r.points();
211 if let Some(first) = points.next() {
212 let last = points.last().unwrap_or(first);
213 acc(last, first);
214 }
215 }
216
217 (sum_a2, sum_x, sum_y)
218}
219
220// ---- Ring ------------------------------------------------------------
221//
222// Mirrors `strategy::centroid::bashein_detmer::result` at
223// `centroid_bashein_detmer.hpp:202-231`: `Cx = sum_x / (3 * sum_a2)`,
224// `Cy = sum_y / (3 * sum_a2)`. When `sum_a2 == 0` (a degenerate, zero-
225// area ring) Boost's `result` returns `false` and the higher-level
226// `centroid_polygon` falls back to the first ring vertex
227// (`test/algorithms/centroid.cpp:50-57`); we mirror that fallback here.
228
229impl<G> CentroidStrategy<G> for CartesianRingCentroid
230where
231 G: RingTrait,
232 G::Point: PointTrait + PointMut + Default + Copy,
233 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
234 ShoelaceArea: AreaStrategy<G, Out = <G::Point as PointTrait>::Scalar>,
235{
236 type Output = G::Point;
237
238 fn centroid(&self, r: &G) -> G::Point {
239 let (sum_a2, sum_x, sum_y) = bashein_detmer_sums(r);
240 let zero = <G::Point as PointTrait>::Scalar::ZERO;
241 if sum_a2 == zero {
242 // Degenerate ring: fall back to the first vertex
243 // (`centroid.cpp:50-57`). An empty ring yields the origin
244 // (Default), matching a zero-init result point.
245 return r.points().next().copied().unwrap_or_default();
246 }
247 let a3 = three::<<G::Point as PointTrait>::Scalar>() * sum_a2;
248 point_2d::<G::Point>(sum_x / a3, sum_y / a3)
249 }
250}
251
252// ---- Polygon ---------------------------------------------------------
253//
254// Mirrors the polygon arm of `algorithms/centroid.hpp`. Each ring
255// contributes `signed_area_k * centroid_k`; the interior rings arrive
256// with the opposite sign under `ShoelaceArea` (Boost's signed-area
257// convention winds holes opposite the exterior), so a plain sum performs
258// the hole subtraction. The result is `sum_c / sum_area`, degenerating
259// to the exterior ring's first vertex when the total signed area is 0.
260
261impl<G> CentroidStrategy<G> for CartesianPolygonCentroid
262where
263 G: PolygonTrait,
264 G::Point: PointTrait + PointMut + Default + Copy,
265 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
266 ShoelaceArea: AreaStrategy<G::Ring, Out = <G::Point as PointTrait>::Scalar>,
267 CartesianRingCentroid: CentroidStrategy<G::Ring, Output = G::Point>,
268{
269 type Output = G::Point;
270
271 fn centroid(&self, pg: &G) -> G::Point {
272 let zero = <G::Point as PointTrait>::Scalar::ZERO;
273 let mut sum_a2 = zero;
274 let mut sum_x = zero;
275 let mut sum_y = zero;
276
277 let mut fold_ring = |ring: &G::Ring| {
278 let (a2, x, y) = bashein_detmer_sums(ring);
279 sum_a2 = sum_a2 + a2;
280 sum_x = sum_x + x;
281 sum_y = sum_y + y;
282 };
283
284 fold_ring(pg.exterior());
285 for inner in pg.interiors() {
286 fold_ring(inner);
287 }
288
289 if sum_a2 == zero {
290 return pg.exterior().points().next().copied().unwrap_or_default();
291 }
292 let a3 = three::<<G::Point as PointTrait>::Scalar>() * sum_a2;
293 point_2d::<G::Point>(sum_x / a3, sum_y / a3)
294 }
295}
296
297// ---- MultiPolygon ----------------------------------------------------
298//
299// Mirrors the multi-polygon arm of `algorithms/centroid.hpp`, which runs
300// one `centroid_multi` state over every ring of every member and divides
301// once. Same reason the polygon arm accumulates rather than combining
302// per-part centroids: a member with zero area drops out of an
303// area-weighted combine but still contributes to the running numerator,
304// and Boost keeps that contribution.
305
306impl<G> CentroidStrategy<G> for CartesianMultiPolygonCentroid
307where
308 G: MultiPolygonTrait,
309 G::Point: PointTrait + PointMut + Default + Copy,
310 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
311{
312 type Output = G::Point;
313
314 fn centroid(&self, mp: &G) -> G::Point {
315 let zero = <G::Point as PointTrait>::Scalar::ZERO;
316 let mut sum_a2 = zero;
317 let mut sum_x = zero;
318 let mut sum_y = zero;
319 let mut first_point = None;
320
321 for polygon in mp.polygons() {
322 if first_point.is_none() {
323 first_point = polygon.exterior().points().next().copied();
324 }
325 for ring in core::iter::once(polygon.exterior()).chain(polygon.interiors()) {
326 let (a2, x, y) = bashein_detmer_sums(ring);
327 sum_a2 = sum_a2 + a2;
328 sum_x = sum_x + x;
329 sum_y = sum_y + y;
330 }
331 }
332
333 if sum_a2 == zero {
334 return first_point.unwrap_or_default();
335 }
336 let a3 = three::<<G::Point as PointTrait>::Scalar>() * sum_a2;
337 point_2d::<G::Point>(sum_x / a3, sum_y / a3)
338 }
339}
340
341// ---- Linestring ------------------------------------------------------
342//
343// Mirrors the linear arm of `algorithms/centroid.hpp`: each segment
344// contributes `seg_length * midpoint`, summed and divided by the total
345// length. Degenerate (total length 0) falls back to the first point
346// (`centroid.cpp:81-82`).
347
348impl<G> CentroidStrategy<G> for CartesianLinestringCentroid
349where
350 G: LinestringTrait,
351 G::Point: PointTrait + PointMut + Default + Copy,
352 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
353 Pythagoras: DistanceStrategy<G::Point, G::Point, Out = <G::Point as PointTrait>::Scalar>,
354{
355 type Output = G::Point;
356
357 fn centroid(&self, ls: &G) -> G::Point {
358 let zero = <G::Point as PointTrait>::Scalar::ZERO;
359 let half =
360 <G::Point as PointTrait>::Scalar::ONE / two::<<G::Point as PointTrait>::Scalar>();
361 let mut total_len = zero;
362 let mut sum_x = zero;
363 let mut sum_y = zero;
364
365 let it = ls.points();
366 let next = it.clone().skip(1);
367 for (a, b) in it.zip(next) {
368 let seg_len = Pythagoras.distance(a, b);
369 let mid_x = (a.get::<0>() + b.get::<0>()) * half;
370 let mid_y = (a.get::<1>() + b.get::<1>()) * half;
371 total_len = total_len + seg_len;
372 sum_x = sum_x + seg_len * mid_x;
373 sum_y = sum_y + seg_len * mid_y;
374 }
375
376 if total_len == zero {
377 return ls.points().next().copied().unwrap_or_default();
378 }
379 point_2d::<G::Point>(sum_x / total_len, sum_y / total_len)
380 }
381}
382
383// ---- Segment ---------------------------------------------------------
384//
385// Mirrors the segment arm of `algorithms/centroid.hpp`: the midpoint of
386// the two endpoints, per dimension.
387
388impl<G> CentroidStrategy<G> for CartesianSegmentCentroid
389where
390 G: SegmentTrait,
391 G::Point: PointTrait + PointMut + Default + Copy,
392 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
393{
394 type Output = G::Point;
395
396 fn centroid(&self, s: &G) -> G::Point {
397 let a = segment_start(s);
398 let b = segment_end(s);
399 midpoint(&a, &b)
400 }
401}
402
403// ---- Box -------------------------------------------------------------
404//
405// Mirrors `detail::centroid::centroid_box` in
406// `algorithms/centroid.hpp`: the midpoint of the min / max corners, per
407// dimension.
408
409impl<G> CentroidStrategy<G> for CartesianBoxCentroid
410where
411 G: BoxTrait,
412 G::Point: PointTrait + PointMut + Default + Copy,
413 <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
414{
415 type Output = G::Point;
416
417 fn centroid(&self, b: &G) -> G::Point {
418 let lo = box_min(b);
419 let hi = box_max(b);
420 midpoint(&lo, &hi)
421 }
422}
423
424// ---- MultiPoint ------------------------------------------------------
425//
426// Mirrors the pointlike arm of `algorithms/centroid.hpp`: the arithmetic
427// mean of the member points, per dimension. Degenerate (no points) falls
428// back to the origin (a zero-init default point).
429
430impl<G> CentroidStrategy<G> for CartesianMultiPointCentroid
431where
432 G: MultiPointTrait,
433 G::ItemPoint: PointTrait + PointMut + Default + Copy,
434 <<G::ItemPoint as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
435{
436 type Output = G::ItemPoint;
437
438 fn centroid(&self, mp: &G) -> G::ItemPoint {
439 let zero = <G::ItemPoint as PointTrait>::Scalar::ZERO;
440 let mut count = zero;
441 let mut sum_x = zero;
442 let mut sum_y = zero;
443 for p in mp.points() {
444 sum_x = sum_x + p.get::<0>();
445 sum_y = sum_y + p.get::<1>();
446 count = count + <G::ItemPoint as PointTrait>::Scalar::ONE;
447 }
448 if count == zero {
449 return G::ItemPoint::default();
450 }
451 point_2d::<G::ItemPoint>(sum_x / count, sum_y / count)
452 }
453}
454
455/// The per-dimension midpoint of two points — `(a + b) / 2` on
456/// dimensions `0` and `1`. Shared by the [`geometry_trait::Segment`] and [`geometry_trait::Box`] impls,
457/// which are both a two-corner midpoint. 2-D only, matching the rest of
458/// this module and the reference test coverage.
459#[inline]
460fn midpoint<P>(a: &P, b: &P) -> P
461where
462 P: PointTrait + PointMut + Default,
463{
464 let half = P::Scalar::ONE / two::<P::Scalar>();
465 let x = (a.get::<0>() + b.get::<0>()) * half;
466 let y = (a.get::<1>() + b.get::<1>()) * half;
467 point_2d::<P>(x, y)
468}
469
470/// Type-level "which centroid strategy does this geometry *kind* use".
471///
472/// One impl per [`geometry_tag`] kind tag, mapping each tag to its
473/// per-kind [`CentroidStrategy`] struct above. Keyed on the **tag**
474/// (`impl CentroidStrategyForKind for RingTag`) rather than on a concept
475/// blanket (`impl<G: Ring> … for G`, which would overlap its `Polygon`
476/// sibling — E0119) or on the concrete `geometry-model` structs (which
477/// would keep `centroid` model-bound). Distinct tags never conflict, so
478/// the picker is coherent; a concept-adapted foreign type resolves to the
479/// same struct as the equivalent model value because they share a
480/// `Kind`. The `geometry-algorithm::centroid` free function routes
481/// `G → G::Kind → S` through this trait, staying strategy-less while
482/// leaving room for the explicit-strategy `centroid_with`.
483///
484/// # Spherical / geographic centroid — DEFERRED (LA8.T3)
485///
486/// The per-kind impls above are all gated on
487/// `<…::Cs>::Family: SameAs<CartesianFamily>`, so `centroid(&g)` is a
488/// compile error for a spherical or geographic geometry — that is
489/// intentional. Boost's *area* and *azimuth* have exact, published
490/// reference values (which LA8.T1/T2/T4 reproduce), but Boost ships **no
491/// dedicated spherical / geographic centroid test values**: its
492/// `strategies/centroid/spherical.hpp` merely marks `Box` / `Segment`
493/// "not applicable" and otherwise inherits the Cartesian
494/// `centroid_average` (an arithmetic mean of lon/lat, *not* a true
495/// on-sphere centroid). The LA8.T3 stub instead sketches a different
496/// algorithm (project to 3-D unit normals, area-weight, normalise, map
497/// back) with **no reference rows to validate against**.
498///
499/// Per the task's "prefer correctness over coverage — skip + document
500/// rather than ship wrong math" directive, the non-Cartesian centroid is
501/// deferred until a validated reference exists. Callers who need it today
502/// can supply an explicit strategy through
503/// `geometry_algorithm::centroid_with`. The `DefaultLength` /
504/// `DefaultArea` / `DefaultAzimuth` family-keyed dispatch traits added in
505/// LA8 give the eventual family impl a ready-made shape to follow.
506#[doc(hidden)]
507pub trait CentroidStrategyForKind {
508 /// The per-kind [`CentroidStrategy`] struct this tag is computed with.
509 type S: Default;
510}
511
512impl CentroidStrategyForKind for RingTag {
513 type S = CartesianRingCentroid;
514}
515
516impl CentroidStrategyForKind for MultiPolygonTag {
517 type S = CartesianMultiPolygonCentroid;
518}
519
520impl CentroidStrategyForKind for PolygonTag {
521 type S = CartesianPolygonCentroid;
522}
523
524impl CentroidStrategyForKind for LinestringTag {
525 type S = CartesianLinestringCentroid;
526}
527
528impl CentroidStrategyForKind for SegmentTag {
529 type S = CartesianSegmentCentroid;
530}
531
532impl CentroidStrategyForKind for BoxTag {
533 type S = CartesianBoxCentroid;
534}
535
536impl CentroidStrategyForKind for MultiPointTag {
537 type S = CartesianMultiPointCentroid;
538}
539
540#[cfg(test)]
541mod tests {
542 //! Reference values from `geometry/test/algorithms/centroid.cpp`.
543 //! `BOOST_CHECK_CLOSE` there uses a 0.0001 % tolerance; the exact
544 //! reference doubles are reproduced with `1e-9` absolute tolerance.
545 #![allow(
546 clippy::float_cmp,
547 reason = "centroids are compared with an explicit absolute tolerance, not `==`"
548 )]
549
550 use super::{
551 CartesianBoxCentroid, CartesianLinestringCentroid, CartesianMultiPointCentroid,
552 CartesianMultiPolygonCentroid, CartesianPolygonCentroid, CartesianRingCentroid,
553 CartesianSegmentCentroid, CentroidStrategy,
554 };
555 use geometry_cs::Cartesian;
556 use geometry_model::{
557 Box, MultiPoint, MultiPolygon, Point2D, Polygon, Ring, Segment, linestring, polygon,
558 };
559 use geometry_trait::Point as _;
560
561 type Pt = Point2D<f64, Cartesian>;
562
563 fn close_pt(got: &Pt, x: f64, y: f64, tol: f64) -> bool {
564 (got.get::<0>() - x).abs() < tol && (got.get::<1>() - y).abs() < tol
565 }
566
567 // centroid.cpp:139 — ring "POLYGON((1 1, 1 2, 2 2, 2 1, 1 1))" → (1.5, 1.5)
568 #[test]
569 fn ring_centroid_unit_square_shift() {
570 let r: Ring<Pt> = Ring::from_vec(vec![
571 Pt::new(1., 1.),
572 Pt::new(1., 2.),
573 Pt::new(2., 2.),
574 Pt::new(2., 1.),
575 Pt::new(1., 1.),
576 ]);
577 let c = CartesianRingCentroid.centroid(&r);
578 assert!(close_pt(&c, 1.5, 1.5, 1e-9));
579 }
580
581 // centroid.cpp:111-114 — the Bashein/Detmer reference ring →
582 // (4.06923363095238, 1.65055803571429).
583 #[test]
584 fn ring_bashein_detmer_reference() {
585 let r: Ring<Pt> = Ring::from_vec(vec![
586 Pt::new(2., 1.3),
587 Pt::new(2.4, 1.7),
588 Pt::new(2.8, 1.8),
589 Pt::new(3.4, 1.2),
590 Pt::new(3.7, 1.6),
591 Pt::new(3.4, 2.),
592 Pt::new(4.1, 3.),
593 Pt::new(5.3, 2.6),
594 Pt::new(5.4, 1.2),
595 Pt::new(4.9, 0.8),
596 Pt::new(2.9, 0.7),
597 Pt::new(2., 1.3),
598 ]);
599 let c = CartesianRingCentroid.centroid(&r);
600 assert!(close_pt(
601 &c,
602 4.069_233_630_952_38,
603 1.650_558_035_714_29,
604 1e-9
605 ));
606 }
607
608 // centroid.cpp:46 — POLYGON((0 0,0 10,10 10,10 0,0 0)) → (5, 5)
609 #[test]
610 fn polygon_10x10_square_centroid_is_5_5() {
611 let pg: Polygon<Pt> = polygon![[(0., 0.), (0., 10.), (10., 10.), (10., 0.), (0., 0.)]];
612 let c = CartesianPolygonCentroid.centroid(&pg);
613 assert!(close_pt(&c, 5.0, 5.0, 1e-9));
614 }
615
616 // centroid.cpp:191-192 — POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), ()) → (0.5, 0.5).
617 // (Unit square, plus an empty interior ring is a no-op.)
618 #[test]
619 fn polygon_unit_square_centroid_is_half_half() {
620 let pg: Polygon<Pt> = polygon![[(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)]];
621 let c = CartesianPolygonCentroid.centroid(&pg);
622 assert!(close_pt(&c, 0.5, 0.5, 1e-9));
623 }
624
625 // centroid.cpp:40-44 — the Bashein/Detmer reference polygon *with a
626 // hole*. The C++ test asserts SQL Server's constant
627 // `(4.0466264962959677, 1.6348996057331333)` with a 0.0001 %
628 // `BOOST_CHECK_CLOSE` tolerance. Boost's own Bashein/Detmer kernel
629 // (which this mirrors) produces the PostGIS / Oracle value
630 // `(4.0466265060241, 1.63489959839357)` quoted at
631 // `centroid_bashein_detmer.hpp:99` — the two agree to ~1e-8, well
632 // inside 0.0001 %. We assert the value the algorithm actually
633 // computes (PostGIS / Oracle) so the tolerance can stay tight.
634 #[test]
635 fn polygon_with_hole_reference() {
636 let pg: Polygon<Pt> = polygon![
637 [
638 (2., 1.3),
639 (2.4, 1.7),
640 (2.8, 1.8),
641 (3.4, 1.2),
642 (3.7, 1.6),
643 (3.4, 2.),
644 (4.1, 3.),
645 (5.3, 2.6),
646 (5.4, 1.2),
647 (4.9, 0.8),
648 (2.9, 0.7),
649 (2., 1.3)
650 ],
651 [(4., 2.), (4.2, 1.4), (4.8, 1.9), (4.4, 2.2), (4., 2.)]
652 ];
653 let c = CartesianPolygonCentroid.centroid(&pg);
654 assert!(close_pt(
655 &c,
656 4.046_626_506_024_1,
657 1.634_899_598_393_57,
658 1e-9
659 ));
660 }
661
662 // centroid.cpp:50 — invalid, self-intersecting (area = 0) polygon →
663 // fall back to first vertex (1, 1).
664 #[test]
665 fn degenerate_zero_area_polygon_returns_first_vertex() {
666 let pg: Polygon<Pt> = polygon![[
667 (1., 1.),
668 (4., -2.),
669 (4., 2.),
670 (10., 0.),
671 (1., 0.),
672 (10., 1.),
673 (1., 1.)
674 ]];
675 let c = CartesianPolygonCentroid.centroid(&pg);
676 assert!(close_pt(&c, 1.0, 1.0, 1e-9));
677 }
678
679 // centroid.cpp:73 — LINESTRING(1 1, 2 2, 3 3) → (2, 2)
680 #[test]
681 fn linestring_centroid_diagonal() {
682 let ls = linestring![(1., 1.), (2., 2.), (3., 3.)];
683 let c = CartesianLinestringCentroid.centroid(&ls);
684 assert!(close_pt(&c, 2.0, 2.0, 1e-9));
685 }
686
687 // centroid.cpp:74 — LINESTRING(0 0,0 4, 4 4) → (1, 3)
688 #[test]
689 fn linestring_centroid_bent() {
690 let ls = linestring![(0., 0.), (0., 4.), (4., 4.)];
691 let c = CartesianLinestringCentroid.centroid(&ls);
692 assert!(close_pt(&c, 1.0, 3.0, 1e-9));
693 }
694
695 // centroid.cpp:81 — degenerate (length 0) linestring → first point.
696 #[test]
697 fn linestring_degenerate_returns_first_point() {
698 let ls = linestring![(1., 1.), (1., 1.)];
699 let c = CartesianLinestringCentroid.centroid(&ls);
700 assert!(close_pt(&c, 1.0, 1.0, 1e-9));
701 }
702
703 // centroid.cpp:109 — segment (1 1) → (3 3) → midpoint (2, 2)
704 #[test]
705 fn segment_midpoint() {
706 let s = Segment::new(Pt::new(1., 1.), Pt::new(3., 3.));
707 let c = CartesianSegmentCentroid.centroid(&s);
708 assert!(close_pt(&c, 2.0, 2.0, 1e-12));
709 }
710
711 // centroid.cpp:131 — box "POLYGON((1 2,3 4))" → (2, 3)
712 #[test]
713 fn box_centroid() {
714 let b: Box<Pt> = Box::from_corners(Pt::new(1., 2.), Pt::new(3., 4.));
715 let c = CartesianBoxCentroid.centroid(&b);
716 assert!(close_pt(&c, 2.0, 3.0, 1e-12));
717 }
718
719 // MultiPoint {(0,0),(2,0),(0,2)} → arithmetic mean (2/3, 2/3).
720 #[test]
721 fn multipoint_mean() {
722 let mp: MultiPoint<Pt> =
723 MultiPoint::from_vec(vec![Pt::new(0., 0.), Pt::new(2., 0.), Pt::new(0., 2.)]);
724 let c = CartesianMultiPointCentroid.centroid(&mp);
725 assert!(close_pt(&c, 2.0 / 3.0, 2.0 / 3.0, 1e-9));
726 }
727
728 /// A part with zero area still contributes to the running numerator.
729 ///
730 /// Combining per-part centroids weighted by area drops it — its weight is
731 /// zero — and lands somewhere else. Boost 1.83 on a clockwise
732 /// `model::polygon` / `model::multi_polygon`:
733 ///
734 /// ```text
735 /// bowtie exterior + hole -> (10.6667, 11) area=-4
736 /// zero-area MP -> (0, 0) area=0
737 /// mixed MP -> (11.3333, 11) area=4
738 /// ```
739 #[test]
740 fn a_zero_area_part_still_moves_the_centroid() {
741 // Exterior is a bow-tie: zero area, non-zero numerator.
742 let bowtie_with_hole: Polygon<Pt> = polygon![
743 [(0.0, 0.0), (2.0, 2.0), (2.0, 0.0), (0.0, 2.0), (0.0, 0.0)],
744 [
745 (10.0, 10.0),
746 (12.0, 10.0),
747 (12.0, 12.0),
748 (10.0, 12.0),
749 (10.0, 10.0)
750 ]
751 ];
752 let c = CartesianPolygonCentroid.centroid(&bowtie_with_hole);
753 assert!(close_pt(&c, 32.0 / 3.0, 11.0, 1e-9), "{c:?}");
754
755 let bowtie: Polygon<Pt> =
756 polygon![[(0.0, 0.0), (2.0, 2.0), (2.0, 0.0), (0.0, 2.0), (0.0, 0.0)]];
757 let other_bowtie: Polygon<Pt> = polygon![[
758 (10.0, 10.0),
759 (12.0, 12.0),
760 (12.0, 10.0),
761 (10.0, 12.0),
762 (10.0, 10.0)
763 ]];
764 let square: Polygon<Pt> = polygon![[
765 (10.0, 10.0),
766 (10.0, 12.0),
767 (12.0, 12.0),
768 (12.0, 10.0),
769 (10.0, 10.0)
770 ]];
771
772 // Every member degenerate: the first vertex of the first member.
773 let all_degenerate = MultiPolygon(vec![bowtie.clone(), other_bowtie]);
774 let c = CartesianMultiPolygonCentroid.centroid(&all_degenerate);
775 assert!(close_pt(&c, 0.0, 0.0, 1e-9), "{c:?}");
776
777 // One degenerate member beside a real one: it still pulls the result.
778 let mixed = MultiPolygon(vec![bowtie, square]);
779 let c = CartesianMultiPolygonCentroid.centroid(&mixed);
780 assert!(close_pt(&c, 34.0 / 3.0, 11.0, 1e-9), "{c:?}");
781 }
782}