1use geometry_cs::CoordinateSystem;
13use geometry_strategy::{
14 DefaultLength, DefaultLengthStrategy, DefaultPerimeter, DefaultPerimeterStrategy,
15 LengthStrategy,
16};
17use geometry_trait::{Geometry, Linestring, Point, Polygon, Ring};
18
19type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
23
24#[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#[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#[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#[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#[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#[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 #![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 #[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(°(4.0, 52.0), °(2.0, 48.0));
217 assert!((got - direct).abs() < 1e-6);
218 assert!((got - 467_270.4).abs() < 1.0, "got {got} m");
220 }
221
222 #[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(°(4.0, 52.0), °(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 #[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 #[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}