Skip to main content

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}