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}