Skip to main content

geometry_algorithm/
length.rs

1//! `length(&g)` and `perimeter(&p)` — see
2//! `boost/geometry/algorithms/{length,perimeter}.hpp`.
3//!
4//! `length` dispatches to the per-CS-family default strategy via
5//! [`geometry_strategy::DefaultLength`] — Cartesian linestrings resolve
6//! to [`geometry_strategy::CartesianLength`], spherical to
7//! [`geometry_strategy::SphericalLength`], geographic to
8//! [`geometry_strategy::GeographicLength`]. `perimeter` and
9//! `ring_perimeter` use the parallel
10//! [`geometry_strategy::DefaultPerimeter`] family dispatch.
11
12use geometry_cs::CoordinateSystem;
13use geometry_strategy::{
14    DefaultLength, DefaultLengthStrategy, DefaultPerimeter, DefaultPerimeterStrategy,
15    LengthStrategy,
16};
17use geometry_trait::{Geometry, Linestring, Point, Polygon, Ring};
18
19/// Shorthand for the CS family of `G`'s point type. Keeps the `where`
20/// clauses readable; matches the projection in
21/// [`geometry_strategy::DefaultLengthStrategy`].
22type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
23
24/// Sum the per-segment distance along a linestring using the default
25/// strategy for its coordinate-system family.
26///
27/// Mirrors `boost::geometry::length` from
28/// `boost/geometry/algorithms/length.hpp`. The strategy is resolved
29/// via [`geometry_strategy::DefaultLengthStrategy`] — Cartesian input
30/// resolves to the Pythagorean [`geometry_strategy::CartesianLength`]
31/// (identical to the v1 behaviour), spherical to Haversine, geographic
32/// to Andoyer. For an explicit strategy use [`length_with`].
33///
34/// # Behaviour on "wrong" kinds
35///
36/// This *static* entry point deliberately compile-errors on a `Point`
37/// or `Polygon` argument
38/// (Boost returns 0 at runtime; when the kind is known
39/// at compile time a type error is a clearer signal). The runtime
40/// contract — Boost's "length of a non-linear kind is 0"
41/// (`length.hpp:75-80`) — is honoured on the *dynamic* path instead:
42/// [`crate::length_dyn`] returns `0` for `Point`,
43/// `Polygon`, `MultiPoint`, ….
44#[inline]
45#[must_use]
46pub fn length<G>(g: &G) -> <DefaultLengthStrategy<G> as LengthStrategy<G>>::Out
47where
48    G: Linestring,
49    Family<G>: DefaultLength<Family<G>>,
50    DefaultLengthStrategy<G>: LengthStrategy<G> + Default,
51{
52    DefaultLengthStrategy::<G>::default().length(g)
53}
54
55/// Length of a linestring using an explicitly supplied strategy.
56///
57/// Mirrors the `length(g, strategy)` overload at
58/// `boost/geometry/algorithms/length.hpp`. Taking the strategy by
59/// value matches the by-value call shape of
60/// [`crate::distance_with`]; concrete strategies are zero-sized or
61/// small `Copy` configuration objects, so this monomorphises into
62/// nothing.
63#[inline]
64#[must_use]
65#[allow(
66    clippy::needless_pass_by_value,
67    reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
68)]
69pub fn length_with<G, S>(g: &G, s: S) -> S::Out
70where
71    G: Geometry,
72    S: LengthStrategy<G>,
73{
74    s.length(g)
75}
76
77/// Perimeter of a polygon: length of the exterior ring plus the sum
78/// of the lengths of the interior rings.
79///
80/// Mirrors `boost::geometry::perimeter` from
81/// `boost/geometry/algorithms/perimeter.hpp:230-236`.
82#[inline]
83#[must_use]
84pub fn perimeter<P>(p: &P) -> <DefaultPerimeterStrategy<P> as LengthStrategy<P::Ring>>::Out
85where
86    P: Polygon,
87    Family<P>: DefaultPerimeter<Family<P>>,
88    DefaultPerimeterStrategy<P>: LengthStrategy<P::Ring> + Default,
89{
90    perimeter_with(p, DefaultPerimeterStrategy::<P>::default())
91}
92
93/// Perimeter of a polygon using an explicitly supplied ring-length strategy.
94///
95/// Mirrors the strategy overload of `boost::geometry::perimeter` from
96/// `boost/geometry/algorithms/perimeter.hpp:252-258`.
97#[inline]
98#[must_use]
99#[allow(
100    clippy::needless_pass_by_value,
101    reason = "perimeter strategies are zero-sized or small Copy values, matching length_with"
102)]
103pub fn perimeter_with<P, S>(p: &P, strategy: S) -> S::Out
104where
105    P: Polygon,
106    S: LengthStrategy<P::Ring>,
107{
108    let mut total = strategy.length(p.exterior());
109    for inner in p.interiors() {
110        total = total + strategy.length(inner);
111    }
112    total
113}
114
115/// Standalone helper to compute the perimeter of a `Ring` directly,
116/// without wrapping it in a polygon. Mirrors Boost's
117/// `perimeter(ring)` overload which dispatches via the
118/// `ring_tag` arm of `algorithms/perimeter.hpp:66-73`.
119#[inline]
120#[must_use]
121pub fn ring_perimeter<R>(r: &R) -> <DefaultPerimeterStrategy<R> as LengthStrategy<R>>::Out
122where
123    R: Ring,
124    Family<R>: DefaultPerimeter<Family<R>>,
125    DefaultPerimeterStrategy<R>: LengthStrategy<R> + Default,
126{
127    ring_perimeter_with(r, DefaultPerimeterStrategy::<R>::default())
128}
129
130/// Perimeter of a ring using an explicitly supplied strategy.
131///
132/// Mirrors the strategy overload of the ring-tag arm in
133/// `boost/geometry/algorithms/perimeter.hpp:252-258`.
134#[inline]
135#[must_use]
136#[allow(
137    clippy::needless_pass_by_value,
138    reason = "perimeter strategies are zero-sized or small Copy values, matching length_with"
139)]
140pub fn ring_perimeter_with<R, S>(ring: &R, strategy: S) -> S::Out
141where
142    R: Ring,
143    S: LengthStrategy<R>,
144{
145    strategy.length(ring)
146}
147
148#[cfg(test)]
149mod tests {
150    //! Reference values from `geometry/test/algorithms/length/length.cpp`
151    //! (lines 24-33) and the classic 4x3 rectangle perimeter case.
152    //! Spherical / geographic dispatch cross-checks the free-function
153    //! result against a direct Haversine / Andoyer call
154    //! (`length_sph.cpp` / `length_geo.cpp`).
155    #![allow(
156        clippy::float_cmp,
157        reason = "lengths are compared with an explicit absolute tolerance, not `==`"
158    )]
159
160    use super::{length, length_with, perimeter, ring_perimeter};
161    use geometry_cs::Cartesian;
162    use geometry_model::{Linestring, Point2D, Ring, linestring, polygon};
163
164    #[test]
165    fn length_of_3_4_5_segment() {
166        let ls: Linestring<Point2D<f64, Cartesian>> = linestring![(0.0, 0.0), (3.0, 4.0)];
167        let got = length(&ls);
168        assert!((got - 5.0).abs() < 1e-12);
169    }
170
171    #[test]
172    fn length_of_three_point_polyline() {
173        let ls: Linestring<Point2D<f64, Cartesian>> =
174            linestring![(0.0, 0.0), (3.0, 4.0), (4.0, 3.0)];
175        let got = length(&ls);
176        let expected = 5.0 + 2.0_f64.sqrt();
177        assert!((got - expected).abs() < 1e-12);
178    }
179
180    #[test]
181    fn perimeter_of_4_3_rectangle() {
182        let p: geometry_model::Polygon<Point2D<f64, Cartesian>> =
183            polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 3.0), (0.0, 3.0), (0.0, 0.0)]];
184        let got = perimeter(&p);
185        assert!((got - 14.0).abs() < 1e-12);
186    }
187
188    #[test]
189    fn ring_perimeter_of_4_3_rectangle() {
190        let mut r = Ring::<Point2D<f64, Cartesian>>::new();
191        r.push(Point2D::new(0.0, 0.0));
192        r.push(Point2D::new(4.0, 0.0));
193        r.push(Point2D::new(4.0, 3.0));
194        r.push(Point2D::new(0.0, 3.0));
195        r.push(Point2D::new(0.0, 0.0));
196        let got = ring_perimeter(&r);
197        assert!((got - 14.0).abs() < 1e-12);
198    }
199
200    /// `length_sph.cpp` — strategy-less `length` on a spherical
201    /// linestring resolves to Haversine. Uses `haversine.cpp:66-67`'s
202    /// headline `(4, 52) → (2, 48) ≈ 467_270.4 m` reference, and
203    /// cross-checks against a direct Haversine call.
204    #[cfg(feature = "std")]
205    #[test]
206    fn spherical_length_dispatches_to_haversine() {
207        use geometry_adapt::{Adapt, WithCs};
208        use geometry_cs::{Degree, Spherical};
209        use geometry_strategy::{DistanceStrategy, spherical::Haversine};
210
211        type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
212        let deg = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
213
214        let ls: Linestring<Sp> = Linestring(vec![deg(4.0, 52.0), deg(2.0, 48.0)]);
215        let got = length(&ls);
216        let direct = Haversine::EARTH.distance(&deg(4.0, 52.0), &deg(2.0, 48.0));
217        assert!((got - direct).abs() < 1e-6);
218        // `haversine.cpp:66-67` — Amsterdam → Paris ≈ 467_270.4 m.
219        assert!((got - 467_270.4).abs() < 1.0, "got {got} m");
220    }
221
222    /// `length_geo.cpp` — strategy-less `length` on a geographic
223    /// linestring resolves to Andoyer; `(4, 52) → (3, 40)` on WGS84
224    /// ≈ 1336.040 km (`andoyer.cpp:230-231`).
225    #[cfg(feature = "std")]
226    #[test]
227    fn geographic_length_dispatches_to_andoyer() {
228        use geometry_adapt::{Adapt, WithCs};
229        use geometry_cs::{Degree, Geographic};
230        use geometry_strategy::{DistanceStrategy, geographic::Andoyer};
231
232        type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
233        let deg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
234
235        let ls: Linestring<Gg> = Linestring(vec![deg(4.0, 52.0), deg(3.0, 40.0)]);
236        let got = length(&ls);
237        let direct = Andoyer::WGS84.distance(&deg(4.0, 52.0), &deg(3.0, 40.0));
238        assert!((got - direct).abs() < 1e-6);
239        assert!((got / 1000.0 - 1_336.039_890).abs() < 0.01);
240    }
241
242    /// `length_with` with an explicit spherical strategy reaches the
243    /// same value as the default dispatch.
244    #[cfg(feature = "std")]
245    #[test]
246    fn length_with_explicit_spherical_strategy() {
247        use geometry_adapt::{Adapt, WithCs};
248        use geometry_cs::{Degree, Spherical};
249        use geometry_strategy::SphericalLength;
250
251        type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
252        let deg = |lon: f64, lat: f64| -> Sp { WithCs::new(Adapt([lon, lat])) };
253
254        let ls: Linestring<Sp> = Linestring(vec![deg(0., 0.), deg(0., 10.)]);
255        let via_with = length_with(&ls, SphericalLength::default());
256        let via_default = length(&ls);
257        assert!((via_with - via_default).abs() < 1e-9);
258    }
259
260    /// `length.hpp:89` — an empty or single-point linestring has length
261    /// 0 (Boost's default-constructed sum), and so does a degenerate
262    /// open ring's perimeter.
263    #[test]
264    fn degenerate_inputs_have_zero_length() {
265        let empty: Linestring<Point2D<f64, Cartesian>> = linestring![];
266        assert_eq!(length(&empty), 0.0);
267        let single: Linestring<Point2D<f64, Cartesian>> = linestring![(3.0, 4.0)];
268        assert_eq!(length(&single), 0.0);
269        let empty_ring = Ring::<Point2D<f64, Cartesian>, true, false>::new();
270        assert_eq!(ring_perimeter(&empty_ring), 0.0);
271    }
272}