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 it = g.points();
136 if let Some(first) = it.next() {
137 if let Some(last) = g.points().last() {
138 total = total + Pythagoras.distance(last, first);
139 }
140 }
141 }
142 total
143 }
144}
145
146/// Walk `it` summing Pythagorean distances between consecutive
147/// points. Returns `P::Scalar::ZERO` for an empty or single-point
148/// range — same as Boost's `range_length` whose initial sum is
149/// default-constructed (`return_type sum = return_type();`,
150/// `algorithms/length.hpp:89`).
151#[inline]
152fn sum_pairwise<'a, P, I>(it: I) -> P::Scalar
153where
154 P: Point + 'a,
155 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
156 I: IntoIterator<Item = &'a P>,
157 I::IntoIter: Clone,
158{
159 let it = it.into_iter();
160 let next = it.clone().skip(1);
161 it.zip(next).fold(P::Scalar::ZERO, |acc, (a, b)| {
162 acc + Pythagoras.distance(a, b)
163 })
164}
165
166// Zero-on-mismatched-kind (Boost `length.hpp:75-80`: length of a
167// point / polygon / multi-point is 0) is NOT expressed as a static
168// `LengthStrategy` impl here: `CartesianLength` carries a blanket
169// `impl<L: Linestring>` (so downstream `register_linestring!` types
170// work), and Rust coherence forbids adding a disjoint concrete-type
171// impl alongside a blanket one — the compiler cannot prove a foreign
172// `Point`/`Polygon` will never also implement `Linestring`. The
173// zero-length contract therefore lives on the *dynamic* path only:
174// `geometry_algorithm::length_dyn` returns 0 for the non-linear arms
175// (KC4.T1). The static `length<G: Linestring>` keeps v1's
176// compile-error stance for non-linear kinds, which is a clearer signal
177// when the kind is known at compile time.
178
179// ---- Default length strategy per CS family --------------------------
180
181/// "Which length strategy do we pick by default for this CS family?"
182///
183/// Mirrors v1's [`DefaultDistance`](crate::distance::DefaultDistance)
184/// for the length algorithm — the Rust analogue of Boost's
185/// `services::default_strategy<Geometry, cs_tag>` in
186/// `strategies/length/services.hpp`, specialised per CS in
187/// `strategies/length/{cartesian,spherical,geographic}.hpp`.
188///
189/// Length is unary, so — unlike `DefaultDistance` — there is exactly
190/// one family type parameter (there is no second geometry). Each
191/// family reports its default length strategy:
192///
193/// ```ignore
194/// impl DefaultLength<CartesianFamily> for CartesianFamily { type Strategy = CartesianLength; }
195/// impl DefaultLength<SphericalFamily> for SphericalFamily { type Strategy = SphericalLength; }
196/// impl DefaultLength<GeographicFamily> for GeographicFamily { type Strategy = GeographicLength; }
197/// ```
198///
199/// The `Strategy: Default` bound matches Boost's expectation that
200/// `services::default_strategy<...>::type` is default-constructible.
201pub trait DefaultLength<Family> {
202 /// The length strategy chosen for this family. Must implement
203 /// [`Default`] because the free-function `length(g)` builds it
204 /// without arguments.
205 type Strategy: Default;
206}
207
208/// Cartesian family defaults to [`CartesianLength`].
209///
210/// Mirrors the `services::default_strategy<Geometry, cartesian_tag>`
211/// specialisation in `strategies/length/cartesian.hpp`.
212impl DefaultLength<CartesianFamily> for CartesianFamily {
213 type Strategy = CartesianLength;
214}
215
216/// Spherical family defaults to [`SphericalLength`](crate::spherical::SphericalLength).
217///
218/// Mirrors the `services::default_strategy<Geometry, spherical_tag>`
219/// specialisation in `strategies/length/spherical.hpp`.
220impl DefaultLength<SphericalFamily> for SphericalFamily {
221 type Strategy = crate::spherical::SphericalLength;
222}
223
224/// Geographic family defaults to [`GeographicLength`](crate::geographic::GeographicLength).
225///
226/// Mirrors the `services::default_strategy<Geometry, geographic_tag>`
227/// specialisation in `strategies/length/geographic.hpp`.
228impl DefaultLength<GeographicFamily> for GeographicFamily {
229 type Strategy = crate::geographic::GeographicLength;
230}
231
232/// Type alias resolving the default length strategy for geometry `G`
233/// by walking `G -> G::Point -> Cs -> Family -> DefaultLength::Strategy`.
234///
235/// Mirrors [`DefaultDistanceStrategy`](crate::distance::DefaultDistanceStrategy)
236/// for the length algorithm; the free-function `length(g)`
237/// monomorphises against this at the call site.
238pub type DefaultLengthStrategy<G> =
239 <<<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultLength<
240 <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
241 >>::Strategy;
242
243#[cfg(test)]
244mod tests {
245 //! Reference values from `geometry/test/algorithms/length/length.cpp`
246 //! (lines 24-33) and the rectangle perimeter case from
247 //! `algorithms/perimeter.cpp` (the classic 4x3 unit-rectangle
248 //! example). Each test cites the source it mirrors.
249
250 use super::{CartesianLength, CartesianPerimeter, LengthStrategy};
251 use geometry_cs::Cartesian;
252 use geometry_model::{Linestring, Point2D, Ring, linestring};
253
254 /// `length.cpp:24` — 3-4-5 segment as a two-point linestring.
255 #[test]
256 fn linestring_3_4_5() {
257 let ls: Linestring<Point2D<f64, Cartesian>> = linestring![(0.0, 0.0), (3.0, 4.0)];
258 let got = CartesianLength.length(&ls);
259 assert!((got - 5.0).abs() < 1e-12);
260 }
261
262 /// `length.cpp:27` — three-point polyline, 5 + sqrt(2).
263 #[test]
264 fn linestring_5_plus_sqrt2() {
265 let ls: Linestring<Point2D<f64, Cartesian>> =
266 linestring![(0.0, 0.0), (3.0, 4.0), (4.0, 3.0)];
267 let got = CartesianLength.length(&ls);
268 let expected = 5.0 + 2.0_f64.sqrt();
269 assert!((got - expected).abs() < 1e-12);
270 }
271
272 /// Closed ring around a 4x3 rectangle: perimeter is 14.
273 #[test]
274 fn closed_ring_4_3_rectangle() {
275 let mut r = Ring::<Point2D<f64, Cartesian>>::new();
276 r.push(Point2D::new(0.0, 0.0));
277 r.push(Point2D::new(4.0, 0.0));
278 r.push(Point2D::new(4.0, 3.0));
279 r.push(Point2D::new(0.0, 3.0));
280 r.push(Point2D::new(0.0, 0.0));
281 let got = CartesianPerimeter.length(&r);
282 assert!((got - 14.0).abs() < 1e-12);
283 }
284
285 /// Open ring (no repeated closing vertex) around the same
286 /// rectangle: the strategy must add the implicit last->first edge.
287 #[test]
288 fn open_ring_4_3_rectangle() {
289 let mut r = Ring::<Point2D<f64, Cartesian>, true, false>::new();
290 r.push(Point2D::new(0.0, 0.0));
291 r.push(Point2D::new(4.0, 0.0));
292 r.push(Point2D::new(4.0, 3.0));
293 r.push(Point2D::new(0.0, 3.0));
294 let got = CartesianPerimeter.length(&r);
295 assert!((got - 14.0).abs() < 1e-12);
296 }
297
298 // KC1.T2 witness: proves this strategy accepts a geometry whose
299 // `Point` is read-only (need not implement `PointMut`). If it
300 // compiles, the read-only bound is locked.
301 fn _accepts_readonly_point<G, S>(s: &S, g: &G) -> S::Out
302 where
303 G: geometry_trait::Geometry,
304 <G as geometry_trait::Geometry>::Point: geometry_trait::Point,
305 S: LengthStrategy<G>,
306 {
307 s.length(g)
308 }
309}