Skip to main content

geometry_strategy/geographic/
length.rs

1//! Geographic geodesic 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 `Geographic`. The Boost geographic length strategy is
6//! `strategies::length::geographic` from
7//! `boost/geometry/strategies/length/geographic.hpp`, which hands a
8//! geodesic inverse formula to the per-segment walk; we mirror that —
9//! [`GeographicLength`] / [`GeographicPerimeter`] are thin shells
10//! around the existing [`Andoyer`] distance
11//! strategy.
12
13use alloc::vec::Vec;
14
15use geometry_cs::{CoordinateSystem, GeographicFamily};
16use geometry_tag::SameAs;
17use geometry_trait::{Closure, Linestring, Point, Ring};
18
19use crate::distance::DistanceStrategy;
20use crate::geographic::Andoyer;
21use crate::length::LengthStrategy;
22
23/// Geographic length: sum of Andoyer geodesic distances between
24/// consecutive points.
25///
26/// Carries the same `spheroid` parameter as
27/// [`Andoyer`] so callers can pick WGS84 /
28/// GDA / custom. `Default::default()` produces
29/// `GeographicLength { andoyer: Andoyer::WGS84 }`.
30///
31/// Mirrors `boost::geometry::strategies::length::geographic<>` from
32/// `strategies/length/geographic.hpp`.
33#[derive(Debug, Default, Clone, Copy)]
34pub struct GeographicLength {
35    /// The inner Andoyer geodesic kernel summed over each segment.
36    pub andoyer: Andoyer,
37}
38
39/// Geographic perimeter — same as [`GeographicLength`] but adds the
40/// closing edge for an open ring.
41///
42/// Separate from [`GeographicLength`] 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.
46#[derive(Debug, Default, Clone, Copy)]
47pub struct GeographicPerimeter {
48    /// The inner Andoyer geodesic kernel summed over each segment.
49    pub andoyer: Andoyer,
50}
51
52// ---- Linestring ------------------------------------------------------
53
54impl<L> LengthStrategy<L> for GeographicLength
55where
56    L: Linestring,
57    <L::Point as Point>::Cs: CoordinateSystem,
58    <<L::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
59    Andoyer: DistanceStrategy<L::Point, L::Point, Out = <L::Point as Point>::Scalar>,
60{
61    type Out = <L::Point as Point>::Scalar;
62
63    #[inline]
64    fn length(&self, g: &L) -> Self::Out {
65        let pts: Vec<&L::Point> = g.points().collect();
66        let mut acc = <Self::Out as geometry_coords::CoordinateScalar>::ZERO;
67        for w in pts.windows(2) {
68            acc = acc + self.andoyer.distance(w[0], w[1]);
69        }
70        acc
71    }
72}
73
74// ---- Ring ------------------------------------------------------------
75
76impl<R> LengthStrategy<R> for GeographicPerimeter
77where
78    R: Ring,
79    <R::Point as Point>::Cs: CoordinateSystem,
80    <<R::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
81    Andoyer: DistanceStrategy<R::Point, R::Point, Out = <R::Point as Point>::Scalar>,
82{
83    type Out = <R::Point as Point>::Scalar;
84
85    #[inline]
86    fn length(&self, g: &R) -> Self::Out {
87        let pts: Vec<&R::Point> = g.points().collect();
88        let mut acc = <Self::Out as geometry_coords::CoordinateScalar>::ZERO;
89        for w in pts.windows(2) {
90            acc = acc + self.andoyer.distance(w[0], w[1]);
91        }
92        match (g.closure(), pts.first(), pts.last()) {
93            (Closure::Open, Some(first), Some(last)) => acc + self.andoyer.distance(last, first),
94            _ => acc,
95        }
96    }
97}
98
99#[cfg(all(test, feature = "std"))]
100mod tests {
101    //! Reference from `boost/geometry/test/algorithms/length/length_geo.cpp`.
102    //! Values are cross-checked against a direct [`Andoyer`] call on the
103    //! same segments (the length strategy reuses the distance strategy
104    //! verbatim, so the two must agree exactly).
105    #![allow(
106        clippy::float_cmp,
107        reason = "lengths are compared with an explicit absolute tolerance, not `==`"
108    )]
109
110    use super::{GeographicLength, GeographicPerimeter};
111    use crate::distance::DistanceStrategy;
112    use crate::geographic::Andoyer;
113    use crate::length::LengthStrategy;
114    use geometry_adapt::{Adapt, WithCs};
115    use geometry_cs::{Degree, Geographic};
116    use geometry_model::{Linestring, Ring};
117
118    type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
119
120    #[inline]
121    fn deg(lon: f64, lat: f64) -> Gg {
122        WithCs::new(Adapt([lon, lat]))
123    }
124
125    /// A single-segment linestring's length equals a direct Andoyer
126    /// call — `(4, 52) → (3, 40)` on WGS84 ≈ 1336.040 km
127    /// (`andoyer.cpp:230-231`).
128    #[test]
129    fn ams_geodesic_length() {
130        let ls: Linestring<Gg> = Linestring(vec![deg(4.0, 52.0), deg(3.0, 40.0)]);
131        let got = GeographicLength::default().length(&ls);
132        let direct = Andoyer::WGS84.distance(&deg(4.0, 52.0), &deg(3.0, 40.0));
133        assert!((got - direct).abs() < 1e-6);
134        assert!((got / 1000.0 - 1_336.039_890).abs() < 0.01);
135    }
136
137    /// A three-point linestring sums two geodesic hops.
138    #[test]
139    fn three_point_geodesic() {
140        let ls: Linestring<Gg> = Linestring(vec![deg(0., 0.), deg(0., 10.), deg(0., 20.)]);
141        let got = GeographicLength::default().length(&ls);
142        let a = Andoyer::WGS84;
143        let expected =
144            a.distance(&deg(0., 0.), &deg(0., 10.)) + a.distance(&deg(0., 10.), &deg(0., 20.));
145        assert!((got - expected).abs() < 1e-6);
146    }
147
148    /// An open ring's perimeter adds the closing edge back to the first
149    /// vertex.
150    #[test]
151    fn open_ring_closes_itself() {
152        let mut r = Ring::<Gg, true, false>::new();
153        r.push(deg(0., 0.));
154        r.push(deg(10., 0.));
155        r.push(deg(10., 10.));
156        r.push(deg(0., 10.));
157        let got = GeographicPerimeter::default().length(&r);
158        let a = Andoyer::WGS84;
159        let expected = a.distance(&deg(0., 0.), &deg(10., 0.))
160            + a.distance(&deg(10., 0.), &deg(10., 10.))
161            + a.distance(&deg(10., 10.), &deg(0., 10.))
162            + a.distance(&deg(0., 10.), &deg(0., 0.));
163        assert!((got - expected).abs() < 1e-6);
164    }
165}