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 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}