geometry_strategy/length.rs
1//! Strategy for summing the length of a sequence of points.
2//!
3//! Mirrors three pieces of Boost.Geometry that collaborate to make
4//! `boost::geometry::length(g)` and `boost::geometry::perimeter(g)`
5//! work for any linestring / ring / polygon in any coordinate system:
6//!
7//! * `boost/geometry/strategies/length/services.hpp` — the
8//! `services::default_strategy<G>` metafunction that picks the
9//! per-CS length strategy.
10//! * `boost/geometry/strategies/length/cartesian.hpp` —
11//! `strategies::length::cartesian<>` plus its
12//! `services::default_strategy<Geometry, cartesian_tag>`
13//! specialisation; the Cartesian implementation hands a
14//! `strategy::distance::pythagoras<>` to the algorithm.
15//! * `boost/geometry/algorithms/length.hpp:80-107` —
16//! `detail::length::range_length` walks the iterator pair and sums
17//! the per-segment distances; this file performs the same walk in
18//! Rust against the [`Linestring`] / [`Ring`] traits.
19//!
20//! T33 lands the Cartesian implementation only — Boost's
21//! Spherical/Geographic length strategies arrive alongside the
22//! Haversine / Andoyer / Vincenty distance strategies in later
23//! tasks (T40+).
24
25use geometry_coords::CoordinateScalar;
26use geometry_cs::{CartesianFamily, CoordinateSystem, GeographicFamily, SphericalFamily};
27use geometry_tag::SameAs;
28use geometry_trait::{Closure, Geometry, Linestring, Point, Ring};
29
30use crate::cartesian::Pythagoras;
31use crate::distance::DistanceStrategy;
32
33/// A strategy for computing the length of a sequence of points.
34///
35/// Mirrors the per-CS length-strategy concept declared in
36/// `boost/geometry/strategies/length/services.hpp` and refined per
37/// coordinate system in `strategies/length/{cartesian,spherical,
38/// geographic}.hpp`. The Boost concept exposes a `distance(p1, p2)`
39/// helper that hands the algorithm a point-to-point distance kernel;
40/// the Rust analogue collapses the two layers (strategy + algorithm
41/// walk) into a single method [`LengthStrategy::length`] keyed on the
42/// geometry type, because the walk shape is identical for every CS —
43/// only the inner distance kernel changes.
44///
45/// # Associated items
46///
47/// * [`Self::Out`] — the scalar the length comes back as.
48/// Equivalent to Boost's `default_length_result<Geometry>::type`
49/// (`strategies/default_length_result.hpp`); typically the
50/// coordinate scalar of `G`'s point type.
51pub trait LengthStrategy<G: Geometry> {
52 /// The output scalar type. Typically the geometry's coordinate
53 /// scalar. Mirrors `default_length_result<G>::type` from
54 /// `strategies/default_length_result.hpp`.
55 type Out: CoordinateScalar;
56
57 /// Sum the per-segment distances along `g`.
58 ///
59 /// Mirrors `detail::length::range_length::apply` from
60 /// `algorithms/length.hpp:80-107` together with the CS-specific
61 /// `strategies::length::*::distance` call at
62 /// `strategies/length/cartesian.hpp:33-39`.
63 fn length(&self, g: &G) -> Self::Out;
64}
65
66/// Cartesian length: sum of Pythagorean distances between
67/// consecutive points (linestring case).
68///
69/// Mirrors `boost::geometry::strategies::length::cartesian<>` from
70/// `strategies/length/cartesian.hpp:29-39`. The strategy carries no
71/// state — `cartesian<>::distance(p1, p2)` returns a fresh
72/// `strategy::distance::pythagoras<>` each call, which on the Rust
73/// side is the unit-struct [`Pythagoras`] used directly below.
74#[derive(Debug, Default, Clone, Copy)]
75pub struct CartesianLength;
76
77/// Cartesian perimeter: sum of Pythagorean distances between
78/// consecutive points of a ring, plus the closing edge when the ring
79/// is open.
80///
81/// Separate from [`CartesianLength`] because Rust's coherence rules
82/// cannot prove that a single type does not implement both
83/// [`Linestring`] and [`Ring`]; splitting the strategy keeps the
84/// per-tag dispatch disjoint at the impl level.
85#[derive(Debug, Default, Clone, Copy)]
86pub struct CartesianPerimeter;
87
88// ---- Linestring ------------------------------------------------------
89//
90// Mirrors the `dispatch::length<Geometry, linestring_tag>` arm at
91// `algorithms/length.hpp:132-135`, which inherits from
92// `detail::length::range_length<Geometry, closed>`. A linestring is
93// already "closed" in the range_length sense: the closing edge from
94// last to first is *not* added — that is the perimeter case (rings).
95
96impl<L> LengthStrategy<L> for CartesianLength
97where
98 L: Linestring,
99 <L::Point as Point>::Cs: CoordinateSystem,
100 <<L::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
101{
102 type Out = <L::Point as Point>::Scalar;
103
104 #[inline]
105 fn length(&self, g: &L) -> Self::Out {
106 sum_pairwise::<L::Point, _>(g.points())
107 }
108}
109
110// ---- Ring ------------------------------------------------------------
111//
112// Mirrors the `dispatch::perimeter<Geometry, ring_tag>` arm at
113// `algorithms/perimeter.hpp:66-73`, which inherits from
114// `detail::length::range_length<Geometry, closure<Geometry>::value>`.
115// For a closed ring the closing edge is already encoded as the
116// repeated last point; for an open ring we add the explicit
117// last->first segment. Boost achieves the same via
118// `views::closeable_view` at `algorithms/length.hpp:90`.
119
120impl<R> LengthStrategy<R> for CartesianPerimeter
121where
122 R: Ring,
123 <R::Point as Point>::Cs: CoordinateSystem,
124 <<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
125{
126 type Out = <R::Point as Point>::Scalar;
127
128 #[inline]
129 fn length(&self, g: &R) -> Self::Out {
130 let mut total = sum_pairwise::<R::Point, _>(g.points());
131 if matches!(g.closure(), Closure::Open) {
132 // An open ring leaves the closing edge implicit — add it
133 // explicitly. Mirrors the `closeable_view` wrap at
134 // `algorithms/length.hpp:90`.
135 let mut points = g.points();
136 if let Some(first) = points.next() {
137 let last = points.last().unwrap_or(first);
138 total = total + Pythagoras.distance(last, first);
139 }
140 }
141 total
142 }
143}
144
145/// Walk `it` summing Pythagorean distances between consecutive
146/// points. Returns `P::Scalar::ZERO` for an empty or single-point
147/// range — same as Boost's `range_length` whose initial sum is
148/// default-constructed (`return_type sum = return_type();`,
149/// `algorithms/length.hpp:89`).
150#[inline]
151fn sum_pairwise<'a, P, I>(it: I) -> P::Scalar
152where
153 P: Point + 'a,
154 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
155 I: IntoIterator<Item = &'a P>,
156 I::IntoIter: Clone,
157{
158 let it = it.into_iter();
159 let next = it.clone().skip(1);
160 it.zip(next).fold(P::Scalar::ZERO, |acc, (a, b)| {
161 acc + Pythagoras.distance(a, b)
162 })
163}
164
165// Zero-on-mismatched-kind (Boost `length.hpp:75-80`: length of a
166// point / polygon / multi-point is 0) is NOT expressed as a static
167// `LengthStrategy` impl here: `CartesianLength` carries a blanket
168// `impl<L: Linestring>` (so downstream `register_linestring!` types
169// work), and Rust coherence forbids adding a disjoint concrete-type
170// impl alongside a blanket one — the compiler cannot prove a foreign
171// `Point`/`Polygon` will never also implement `Linestring`. The
172// zero-length contract therefore lives on the *dynamic* path only:
173// `geometry_algorithm::length_dyn` returns 0 for the non-linear arms
174// (KC4.T1). The static `length<G: Linestring>` keeps v1's
175// compile-error stance for non-linear kinds, which is a clearer signal
176// when the kind is known at compile time.
177
178// ---- Default length strategy per CS family --------------------------
179
180/// "Which length strategy do we pick by default for this CS family?"
181///
182/// Mirrors v1's [`DefaultDistance`](crate::distance::DefaultDistance)
183/// for the length algorithm — the Rust analogue of Boost's
184/// `services::default_strategy<Geometry, cs_tag>` in
185/// `strategies/length/services.hpp`, specialised per CS in
186/// `strategies/length/{cartesian,spherical,geographic}.hpp`.
187///
188/// Length is unary, so — unlike `DefaultDistance` — there is exactly
189/// one family type parameter (there is no second geometry). Each
190/// family reports its default length strategy:
191///
192/// ```ignore
193/// impl DefaultLength<CartesianFamily> for CartesianFamily { type Strategy = CartesianLength; }
194/// impl DefaultLength<SphericalFamily> for SphericalFamily { type Strategy = SphericalLength; }
195/// impl DefaultLength<GeographicFamily> for GeographicFamily { type Strategy = GeographicLength; }
196/// ```
197///
198/// The `Strategy: Default` bound matches Boost's expectation that
199/// `services::default_strategy<...>::type` is default-constructible.
200pub trait DefaultLength<Family> {
201 /// The length strategy chosen for this family. Must implement
202 /// [`Default`] because the free-function `length(g)` builds it
203 /// without arguments.
204 type Strategy: Default;
205}
206
207/// "Which perimeter strategy do we pick by default for this CS family?"
208///
209/// Mirrors the default-strategy resolution used by
210/// `boost::geometry::perimeter` in `algorithms/perimeter.hpp:115-159`.
211pub trait DefaultPerimeter<Family> {
212 /// The perimeter strategy chosen for this family.
213 type Strategy: Default;
214}
215
216/// Cartesian family defaults to [`CartesianLength`].
217///
218/// Mirrors the `services::default_strategy<Geometry, cartesian_tag>`
219/// specialisation in `strategies/length/cartesian.hpp`.
220impl DefaultLength<CartesianFamily> for CartesianFamily {
221 type Strategy = CartesianLength;
222}
223
224/// Spherical family defaults to [`SphericalLength`](crate::spherical::SphericalLength).
225///
226/// Mirrors the `services::default_strategy<Geometry, spherical_tag>`
227/// specialisation in `strategies/length/spherical.hpp`.
228impl DefaultLength<SphericalFamily> for SphericalFamily {
229 type Strategy = crate::spherical::SphericalLength;
230}
231
232/// Geographic family defaults to [`GeographicLength`](crate::geographic::GeographicLength).
233///
234/// Mirrors the `services::default_strategy<Geometry, geographic_tag>`
235/// specialisation in `strategies/length/geographic.hpp`.
236impl DefaultLength<GeographicFamily> for GeographicFamily {
237 type Strategy = crate::geographic::GeographicLength;
238}
239
240/// Selects Boost's Cartesian length strategy for perimeter dispatch.
241///
242/// Mirrors `strategies/length/cartesian.hpp:42-49`.
243impl DefaultPerimeter<CartesianFamily> for CartesianFamily {
244 type Strategy = CartesianPerimeter;
245}
246
247/// Selects Boost's spherical length strategy for perimeter dispatch.
248///
249/// Mirrors `strategies/length/spherical.hpp:57-64`.
250impl DefaultPerimeter<SphericalFamily> for SphericalFamily {
251 type Strategy = crate::spherical::SphericalPerimeter;
252}
253
254/// Selects Boost's geographic length strategy for perimeter dispatch.
255///
256/// Mirrors `strategies/length/geographic.hpp:61-68`.
257impl DefaultPerimeter<GeographicFamily> for GeographicFamily {
258 type Strategy = crate::geographic::GeographicPerimeter;
259}
260
261/// Type alias resolving the default length strategy for geometry `G`
262/// by walking `G -> G::Point -> Cs -> Family -> DefaultLength::Strategy`.
263///
264/// Mirrors [`DefaultDistanceStrategy`](crate::distance::DefaultDistanceStrategy)
265/// for the length algorithm; the free-function `length(g)`
266/// monomorphises against this at the call site.
267pub type DefaultLengthStrategy<G> =
268 <<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultLength<
269 <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
270 >>::Strategy;
271
272/// Default perimeter strategy for the coordinate-system family of `G`.
273///
274/// Mirrors the default-strategy projection in
275/// `algorithms/perimeter.hpp:145-158`.
276pub type DefaultPerimeterStrategy<G> =
277 <<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultPerimeter<
278 <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
279 >>::Strategy;
280
281#[cfg(test)]
282mod tests {
283 //! Reference values from `geometry/test/algorithms/length/length.cpp`
284 //! (lines 24-33) and the rectangle perimeter case from
285 //! `algorithms/perimeter.cpp` (the classic 4x3 unit-rectangle
286 //! example). Each test cites the source it mirrors.
287
288 use super::{CartesianLength, CartesianPerimeter, LengthStrategy};
289 use geometry_cs::Cartesian;
290 use geometry_model::{Linestring, Point2D, Ring, linestring};
291
292 /// `length.cpp:24` — 3-4-5 segment as a two-point linestring.
293 #[test]
294 fn linestring_3_4_5() {
295 let ls: Linestring<Point2D<f64, Cartesian>> = linestring![(0.0, 0.0), (3.0, 4.0)];
296 let got = CartesianLength.length(&ls);
297 assert!((got - 5.0).abs() < 1e-12);
298 }
299
300 /// `length.cpp:27` — three-point polyline, 5 + sqrt(2).
301 #[test]
302 fn linestring_5_plus_sqrt2() {
303 let ls: Linestring<Point2D<f64, Cartesian>> =
304 linestring![(0.0, 0.0), (3.0, 4.0), (4.0, 3.0)];
305 let got = CartesianLength.length(&ls);
306 let expected = 5.0 + 2.0_f64.sqrt();
307 assert!((got - expected).abs() < 1e-12);
308 }
309
310 /// Closed ring around a 4x3 rectangle: perimeter is 14.
311 #[test]
312 fn closed_ring_4_3_rectangle() {
313 let mut r = Ring::<Point2D<f64, Cartesian>>::new();
314 r.push(Point2D::new(0.0, 0.0));
315 r.push(Point2D::new(4.0, 0.0));
316 r.push(Point2D::new(4.0, 3.0));
317 r.push(Point2D::new(0.0, 3.0));
318 r.push(Point2D::new(0.0, 0.0));
319 let got = CartesianPerimeter.length(&r);
320 assert!((got - 14.0).abs() < 1e-12);
321 }
322
323 /// Open ring (no repeated closing vertex) around the same
324 /// rectangle: the strategy must add the implicit last->first edge.
325 #[test]
326 fn open_ring_4_3_rectangle() {
327 let mut r = Ring::<Point2D<f64, Cartesian>, true, false>::new();
328 r.push(Point2D::new(0.0, 0.0));
329 r.push(Point2D::new(4.0, 0.0));
330 r.push(Point2D::new(4.0, 3.0));
331 r.push(Point2D::new(0.0, 3.0));
332 let got = CartesianPerimeter.length(&r);
333 assert!((got - 14.0).abs() < 1e-12);
334 }
335
336 // KC1.T2 witness: proves this strategy accepts a geometry whose
337 // `Point` is read-only (need not implement `PointMut`). If it
338 // compiles, the read-only bound is locked.
339 fn _accepts_readonly_point<G, S>(s: &S, g: &G) -> S::Out
340 where
341 G: geometry_trait::Geometry,
342 <G as geometry_trait::Geometry>::Point: geometry_trait::Point,
343 S: LengthStrategy<G>,
344 {
345 s.length(g)
346 }
347
348 /// Invoke the read-only-point witness so its body is exercised too
349 /// (the compile-time guarantee is unchanged; this just runs it).
350 #[test]
351 #[allow(
352 clippy::used_underscore_items,
353 reason = "the test exists to run the compile-time witness's body"
354 )]
355 fn readonly_point_witness_computes_length() {
356 let ls: Linestring<Point2D<f64, Cartesian>> = linestring![(0.0, 0.0), (3.0, 4.0)];
357 let got = _accepts_readonly_point(&CartesianLength, &ls);
358 assert!((got - 5.0).abs() < 1e-12);
359 }
360}