Skip to main content

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