Skip to main content

geometry_strategy/spherical/
length.rs

1//! Spherical great-circle length / perimeter.
2//!
3//! Mirrors the per-CS dispatch arm of
4//! `boost/geometry/algorithms/length.hpp` reached when the geometry's
5//! CS family is `Spherical`. The Boost spherical length strategy is
6//! `strategies::length::spherical` from
7//! `boost/geometry/strategies/length/spherical.hpp`, which hands
8//! `strategy::distance::haversine` to the per-segment walk; we mirror
9//! that — [`SphericalLength`] / [`SphericalPerimeter`] are thin shells
10//! around the existing [`Haversine`]
11//! distance strategy.
12
13use alloc::vec::Vec;
14
15use geometry_cs::{CoordinateSystem, SphericalFamily};
16use geometry_tag::SameAs;
17use geometry_trait::{Closure, Linestring, Point, Ring};
18
19use crate::distance::DistanceStrategy;
20use crate::length::LengthStrategy;
21use crate::spherical::Haversine;
22
23/// Spherical length: sum of Haversine distances between consecutive
24/// points.
25///
26/// Carries the same `radius` parameter as [`Haversine`]
27/// so callers control the sphere size (Earth ≈ `6_372_795` m by Boost
28/// convention). `Default::default()` produces
29/// `SphericalLength { haversine: Haversine::EARTH }`.
30///
31/// Mirrors `boost::geometry::strategies::length::spherical<>` from
32/// `strategies/length/spherical.hpp`.
33#[derive(Debug, Default, Clone, Copy)]
34pub struct SphericalLength {
35    /// The inner Haversine distance kernel summed over each segment.
36    pub haversine: Haversine,
37}
38
39/// Spherical perimeter — same as [`SphericalLength`] but adds the
40/// closing edge for an open ring.
41///
42/// Separate from [`SphericalLength`] because Rust's coherence rules
43/// cannot prove that a single type does not implement both
44/// [`Linestring`] and [`Ring`]; splitting the strategy keeps the
45/// per-tag dispatch disjoint at the impl level (the same split as
46/// [`CartesianLength`](crate::length::CartesianLength) /
47/// [`CartesianPerimeter`](crate::length::CartesianPerimeter)).
48#[derive(Debug, Default, Clone, Copy)]
49pub struct SphericalPerimeter {
50    /// The inner Haversine distance kernel summed over each segment.
51    pub haversine: Haversine,
52}
53
54// ---- Linestring ------------------------------------------------------
55//
56// Mirrors the `dispatch::length<Geometry, linestring_tag>` arm at
57// `algorithms/length.hpp:132-135`, feeding the spherical Haversine
58// kernel instead of Pythagoras.
59
60impl<L> LengthStrategy<L> for SphericalLength
61where
62    L: Linestring,
63    <L::Point as Point>::Cs: CoordinateSystem,
64    <<L::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
65    Haversine: DistanceStrategy<L::Point, L::Point, Out = <L::Point as Point>::Scalar>,
66{
67    type Out = <L::Point as Point>::Scalar;
68
69    #[inline]
70    fn length(&self, g: &L) -> Self::Out {
71        let pts: Vec<&L::Point> = g.points().collect();
72        let mut acc = <Self::Out as geometry_coords::CoordinateScalar>::ZERO;
73        for w in pts.windows(2) {
74            acc = acc + self.haversine.distance(w[0], w[1]);
75        }
76        acc
77    }
78}
79
80// ---- Ring ------------------------------------------------------------
81//
82// Mirrors the `dispatch::perimeter<Geometry, ring_tag>` arm at
83// `algorithms/perimeter.hpp:66-73`. For an open ring the explicit
84// last->first segment is added; a closed ring already encodes the
85// closing edge as its repeated last point.
86
87impl<R> LengthStrategy<R> for SphericalPerimeter
88where
89    R: Ring,
90    <R::Point as Point>::Cs: CoordinateSystem,
91    <<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
92    Haversine: DistanceStrategy<R::Point, R::Point, Out = <R::Point as Point>::Scalar>,
93{
94    type Out = <R::Point as Point>::Scalar;
95
96    #[inline]
97    fn length(&self, g: &R) -> Self::Out {
98        let pts: Vec<&R::Point> = g.points().collect();
99        let mut acc = <Self::Out as geometry_coords::CoordinateScalar>::ZERO;
100        for w in pts.windows(2) {
101            acc = acc + self.haversine.distance(w[0], w[1]);
102        }
103        match (g.closure(), pts.first(), pts.last()) {
104            (Closure::Open, Some(first), Some(last)) => acc + self.haversine.distance(last, first),
105            _ => acc,
106        }
107    }
108}
109
110#[cfg(all(test, feature = "std"))]
111mod tests {
112    //! Reference from `boost/geometry/test/algorithms/length/length_sph.cpp`.
113    //! Values are cross-checked against a direct [`Haversine`] call on
114    //! the same segments (the length strategy reuses the distance
115    //! strategy verbatim, so the two must agree exactly).
116    #![allow(
117        clippy::float_cmp,
118        reason = "lengths are compared with an explicit absolute tolerance, not `==`"
119    )]
120
121    use super::{SphericalLength, SphericalPerimeter};
122    use crate::distance::DistanceStrategy;
123    use crate::length::LengthStrategy;
124    use crate::spherical::Haversine;
125    use geometry_adapt::{Adapt, WithCs};
126    use geometry_cs::{Degree, Spherical};
127    use geometry_model::{Linestring, Ring};
128
129    type Sp = WithCs<Adapt<[f64; 2]>, Spherical<Degree>>;
130
131    #[inline]
132    fn deg(lon: f64, lat: f64) -> Sp {
133        WithCs::new(Adapt([lon, lat]))
134    }
135
136    /// A single-segment linestring's length equals a direct Haversine
137    /// call — Amsterdam → Paris on the Boost average earth.
138    #[test]
139    fn ams_paris_haversine_length() {
140        let ls: Linestring<Sp> = Linestring(vec![deg(4.90, 52.37), deg(2.35, 48.86)]);
141        let got = SphericalLength::default().length(&ls);
142        let direct = Haversine::EARTH.distance(&deg(4.90, 52.37), &deg(2.35, 48.86));
143        assert!((got - direct).abs() < 1e-6);
144    }
145
146    /// A three-point meridian linestring sums two equal 10° hops.
147    #[test]
148    fn three_point_great_circle() {
149        let ls: Linestring<Sp> = Linestring(vec![deg(0., 0.), deg(0., 10.), deg(0., 20.)]);
150        let got = SphericalLength::default().length(&ls);
151        let segment = Haversine::EARTH.distance(&deg(0., 0.), &deg(0., 10.));
152        assert!((got - segment * 2.0).abs() < 1e-6);
153    }
154
155    /// An open ring's perimeter adds the closing edge back to the
156    /// first vertex.
157    #[test]
158    fn open_ring_closes_itself() {
159        let mut r = Ring::<Sp, true, false>::new();
160        r.push(deg(0., 0.));
161        r.push(deg(10., 0.));
162        r.push(deg(10., 10.));
163        r.push(deg(0., 10.));
164        let got = SphericalPerimeter::default().length(&r);
165        let h = Haversine::EARTH;
166        let expected = h.distance(&deg(0., 0.), &deg(10., 0.))
167            + h.distance(&deg(10., 0.), &deg(10., 10.))
168            + h.distance(&deg(10., 10.), &deg(0., 10.))
169            + h.distance(&deg(0., 10.), &deg(0., 0.));
170        assert!((got - expected).abs() < 1e-6);
171    }
172}