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