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