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