1use geometry_cs::CoordinateSystem;
15use geometry_strategy::{CartesianPerimeter, DefaultLength, DefaultLengthStrategy, LengthStrategy};
16use geometry_trait::{Geometry, Linestring, Point, Polygon, Ring};
17
18type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
22
23#[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#[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#[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#[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 #![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 #[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(°(4.0, 52.0), °(2.0, 48.0));
180 assert!((got - direct).abs() < 1e-6);
181 assert!((got - 467_270.4).abs() < 1.0, "got {got} m");
183 }
184
185 #[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(°(4.0, 52.0), °(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 #[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 #[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}