Skip to main content

geometry_strategy/
segmentize.rs

1//! Equal-length linestring subdivision strategies.
2//!
3//! Cartesian subdivision interpolates each edge linearly. The spherical
4//! implementation uses Haversine lengths and great-circle interpolation, so
5//! explicit strategy selection changes both measurement and cut placement.
6
7use 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
21/// Strategy for splitting a linestring into equal-length pieces.
22pub trait SegmentizeStrategy<L> {
23    /// Segmented output geometry.
24    type Output;
25
26    /// Split `line` into `count` pieces.
27    fn segmentize(&self, line: &L, count: usize) -> Self::Output;
28}
29
30/// Cartesian length and linear-interpolation segmentization.
31#[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    // `cumulative` is initialized with the zero-distance origin above.
121    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    // A positive in-range distance selects the first cumulative interval
176    // ending at that distance; zero-length plateaus are therefore skipped.
177    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}