1use alloc::{vec, vec::Vec};
8
9use geometry_cs::{CartesianFamily, CoordinateSystem, SphericalFamily};
10use geometry_model::{Linestring as ModelLinestring, MultiLinestring};
11use geometry_tag::SameAs;
12use geometry_trait::{Linestring, Point, PointMut};
13
14use crate::{DistanceStrategy, Haversine, Pythagoras};
15
16#[cfg(feature = "std")]
17use crate::normalise::{HasAngularUnits, lonlat_radians};
18#[cfg(feature = "std")]
19use geometry_cs::AngleUnit;
20
21pub trait SegmentizeStrategy<L> {
23 type Output;
25
26 fn segmentize(&self, line: &L, count: usize) -> Self::Output;
28}
29
30#[derive(Debug, Default, Clone, Copy)]
32pub struct CartesianSegmentize;
33
34impl<L, P> SegmentizeStrategy<L> for CartesianSegmentize
35where
36 L: Linestring<Point = P>,
37 P: Point<Scalar = f64> + PointMut + Default + Copy,
38 <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
39 Pythagoras: DistanceStrategy<P, P, Out = f64>,
40{
41 type Output = MultiLinestring<ModelLinestring<P>>;
42
43 fn segmentize(&self, line: &L, count: usize) -> Self::Output {
44 segmentize(line, count, self)
45 }
46}
47
48impl<P> SegmentMetric<P> for CartesianSegmentize
49where
50 P: Point<Scalar = f64> + PointMut + Default,
51 Pythagoras: DistanceStrategy<P, P, Out = f64>,
52{
53 fn distance(&self, first: &P, second: &P) -> f64 {
54 Pythagoras.distance(first, second)
55 }
56
57 fn interpolate(&self, first: &P, second: &P, fraction: f64) -> P {
58 linear_interpolate(first, second, fraction)
59 }
60}
61
62#[cfg(feature = "std")]
63impl<L, P> SegmentizeStrategy<L> for Haversine
64where
65 L: Linestring<Point = P>,
66 P: Point<Scalar = f64> + PointMut + Default + Copy,
67 P::Cs: HasAngularUnits,
68 <P::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
69 Haversine: DistanceStrategy<P, P, Out = f64>,
70{
71 type Output = MultiLinestring<ModelLinestring<P>>;
72
73 fn segmentize(&self, line: &L, count: usize) -> Self::Output {
74 segmentize(line, count, self)
75 }
76}
77
78#[cfg(feature = "std")]
79impl<P> SegmentMetric<P> for Haversine
80where
81 P: Point<Scalar = f64> + PointMut + Default,
82 P::Cs: HasAngularUnits,
83 Haversine: DistanceStrategy<P, P, Out = f64>,
84{
85 fn distance(&self, first: &P, second: &P) -> f64 {
86 DistanceStrategy::distance(self, first, second)
87 }
88
89 fn interpolate(&self, first: &P, second: &P, fraction: f64) -> P {
90 great_circle_interpolate(first, second, fraction, self.radius)
91 }
92}
93
94trait SegmentMetric<P> {
95 fn distance(&self, first: &P, second: &P) -> f64;
96 fn interpolate(&self, first: &P, second: &P, fraction: f64) -> P;
97}
98
99#[allow(
100 clippy::cast_precision_loss,
101 reason = "piece counts become normalized f64 fractions"
102)]
103fn segmentize<L, P, M>(line: &L, count: usize, metric: &M) -> MultiLinestring<ModelLinestring<P>>
104where
105 L: Linestring<Point = P>,
106 P: Point<Scalar = f64> + PointMut + Default + Copy,
107 M: SegmentMetric<P>,
108{
109 let points: Vec<P> = line.points().copied().collect();
110 if count == 0 || points.len() < 2 {
111 return MultiLinestring(Vec::new());
112 }
113
114 let mut cumulative = Vec::with_capacity(points.len());
115 cumulative.push(0.0);
116 for edge in points.windows(2) {
117 let next = cumulative.last().copied().unwrap_or(0.0) + metric.distance(&edge[0], &edge[1]);
118 cumulative.push(next);
119 }
120 let total = *cumulative
122 .last()
123 .expect("cumulative distances are non-empty");
124 if total == 0.0 {
125 return MultiLinestring(vec![ModelLinestring::from_vec(points)]);
126 }
127
128 let mut pieces = Vec::with_capacity(count);
129 for piece_index in 0..count {
130 let start_distance = total * piece_index as f64 / count as f64;
131 let end_distance = total * (piece_index + 1) as f64 / count as f64;
132 let mut piece = Vec::new();
133 piece.push(point_at_distance(
134 &points,
135 &cumulative,
136 start_distance,
137 metric,
138 ));
139 for (index, distance) in cumulative.iter().copied().enumerate().skip(1) {
140 if distance > start_distance && distance < end_distance {
141 piece.push(points[index]);
142 }
143 }
144 piece.push(point_at_distance(
145 &points,
146 &cumulative,
147 end_distance,
148 metric,
149 ));
150 pieces.push(ModelLinestring::from_vec(piece));
151 }
152 MultiLinestring(pieces)
153}
154
155fn point_at_distance<P, M>(points: &[P], cumulative: &[f64], distance: f64, metric: &M) -> P
156where
157 P: Point<Scalar = f64> + PointMut + Default + Copy,
158 M: SegmentMetric<P>,
159{
160 if distance <= 0.0 {
161 return points[0];
162 }
163 let total = cumulative
164 .last()
165 .copied()
166 .expect("segmentization builds one cumulative distance per input point");
167 if distance >= total {
168 return *points.last().unwrap_or(&points[0]);
169 }
170 let edge_index = cumulative
171 .windows(2)
172 .position(|range| distance <= range[1])
173 .unwrap_or(cumulative.len().saturating_sub(2));
174 let edge_length = cumulative[edge_index + 1] - cumulative[edge_index];
175 metric.interpolate(
178 &points[edge_index],
179 &points[edge_index + 1],
180 (distance - cumulative[edge_index]) / edge_length,
181 )
182}
183
184fn linear_interpolate<P>(first: &P, second: &P, fraction: f64) -> P
185where
186 P: Point<Scalar = f64> + PointMut + Default,
187{
188 let mut output = P::default();
189 geometry_trait::fold_dims((), first, |(), _, dimension| {
190 let first_value = get_dimension(first, dimension);
191 let second_value = get_dimension(second, dimension);
192 set_dimension(
193 &mut output,
194 dimension,
195 first_value + fraction * (second_value - first_value),
196 );
197 });
198 output
199}
200
201#[cfg(feature = "std")]
202fn great_circle_interpolate<P>(first: &P, second: &P, fraction: f64, radius: f64) -> P
203where
204 P: Point<Scalar = f64> + PointMut + Default,
205 P::Cs: HasAngularUnits,
206 Haversine: DistanceStrategy<P, P, Out = f64>,
207{
208 type Units<P> = <<P as Point>::Cs as HasAngularUnits>::Units;
209 let metric = Haversine { radius };
210 let angle = DistanceStrategy::distance(&metric, first, second) / radius;
211 let sine = angle.sin();
212 if sine.abs() < f64::EPSILON {
213 return linear_interpolate(first, second, fraction);
214 }
215
216 let (longitude1, latitude1) = lonlat_radians(first);
217 let (longitude2, latitude2) = lonlat_radians(second);
218 let first_weight = ((1.0 - fraction) * angle).sin() / sine;
219 let second_weight = (fraction * angle).sin() / sine;
220 let x = first_weight * latitude1.cos() * longitude1.cos()
221 + second_weight * latitude2.cos() * longitude2.cos();
222 let y = first_weight * latitude1.cos() * longitude1.sin()
223 + second_weight * latitude2.cos() * longitude2.sin();
224 let z = first_weight * latitude1.sin() + second_weight * latitude2.sin();
225 let longitude = y.atan2(x);
226 let latitude = z.atan2(x.hypot(y));
227
228 let mut output = linear_interpolate(first, second, fraction);
229 output.set::<0>(Units::<P>::from_radians(longitude));
230 output.set::<1>(Units::<P>::from_radians(latitude));
231 output
232}
233
234fn get_dimension<P: Point<Scalar = f64>>(point: &P, dimension: usize) -> f64 {
235 match dimension {
236 0 => point.get::<0>(),
237 1 => point.get::<1>(),
238 2 => point.get::<2>(),
239 3 => point.get::<3>(),
240 _ => unreachable!("point folds are limited to four dimensions"),
241 }
242}
243
244fn set_dimension<P: PointMut<Scalar = f64>>(point: &mut P, dimension: usize, value: f64) {
245 match dimension {
246 0 => point.set::<0>(value),
247 1 => point.set::<1>(value),
248 2 => point.set::<2>(value),
249 3 => point.set::<3>(value),
250 _ => unreachable!("point folds are limited to four dimensions"),
251 }
252}