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