Skip to main content

geometry_algorithm/
line_interpolate.rs

1//! `line_interpolate(ls, t) -> Point` — point at fractional
2//! arc-length `t ∈ [0, 1]` along `ls`.
3//!
4//! Mirrors `boost::geometry::line_interpolate(ls, length, out)` from
5//! `boost/geometry/algorithms/line_interpolate.hpp`. Boost takes an
6//! *absolute* `length`; the Rust port takes a *fraction* — easier to
7//! reason about and matching the common GIS API
8//! (`PostGIS::ST_LineInterpolatePoint`, Shapely's
9//! `interpolate(normalized=True)`). Values of `t` outside `[0, 1]`
10//! clamp to the endpoints, matching Boost's silent-clamp behaviour.
11
12use geometry_strategy::{CartesianLineInterpolate, LineInterpolateStrategy};
13use geometry_trait::Linestring;
14
15/// Return the point at fractional arc-length `t ∈ [0, 1]` along `ls`,
16/// measured by accumulated Pythagorean segment length.
17///
18/// Mirrors `boost::geometry::line_interpolate` from
19/// `boost/geometry/algorithms/line_interpolate.hpp`. `t = 0` returns
20/// the first point, `t = 1` the last; both short-circuit without
21/// walking the whole linestring.
22#[inline]
23#[must_use]
24pub fn line_interpolate<L>(ls: &L, t: f64) -> L::Point
25where
26    L: Linestring,
27    CartesianLineInterpolate: LineInterpolateStrategy<L>,
28{
29    CartesianLineInterpolate.interpolate(ls, t)
30}
31
32#[cfg(test)]
33#[allow(
34    clippy::float_cmp,
35    reason = "Interpolated coordinates are exact literals."
36)]
37mod tests {
38    //! Reference from
39    //! `boost/geometry/test/algorithms/line_interpolate.cpp:30-75`.
40
41    use super::line_interpolate;
42    use geometry_cs::Cartesian;
43    use geometry_model::{Linestring, Point2D, linestring};
44    use geometry_trait::Point as _;
45
46    type Pt = Point2D<f64, Cartesian>;
47
48    fn close(got: Pt, x: f64, y: f64) -> bool {
49        (got.get::<0>() - x).abs() < 1e-9 && (got.get::<1>() - y).abs() < 1e-9
50    }
51
52    #[test]
53    fn t_zero_returns_first_point() {
54        let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.)];
55        assert!(close(line_interpolate(&ls, 0.0), 0., 0.));
56    }
57
58    #[test]
59    fn t_one_returns_last_point() {
60        let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.)];
61        assert!(close(line_interpolate(&ls, 1.0), 10., 0.));
62    }
63
64    #[test]
65    fn t_half_returns_midpoint() {
66        let ls: Linestring<Pt> = linestring![(0., 0.), (10., 0.)];
67        assert!(close(line_interpolate(&ls, 0.5), 5., 0.));
68    }
69
70    #[test]
71    fn t_at_segment_boundary_returns_vertex() {
72        // total length 2 + 3 = 5; t=0.4 lands at arc-length 2.0 →
73        // exactly the joining vertex (2, 0).
74        let ls: Linestring<Pt> = linestring![(0., 0.), (2., 0.), (2., 3.)];
75        assert!(close(line_interpolate(&ls, 0.4), 2., 0.));
76    }
77
78    #[test]
79    fn t_inside_second_segment() {
80        // t=0.6 lands at arc 3.0 → 1.0 into the second segment → (2, 1).
81        let ls: Linestring<Pt> = linestring![(0., 0.), (2., 0.), (2., 3.)];
82        assert!(close(line_interpolate(&ls, 0.6), 2., 1.));
83    }
84
85    /// An empty linestring returns the default point (the degenerate
86    /// guard); a single-point linestring returns that point.
87    #[test]
88    fn degenerate_inputs() {
89        let empty: Linestring<Pt> = linestring![];
90        assert!(close(line_interpolate(&empty, 0.5), 0., 0.));
91        let single: Linestring<Pt> = linestring![(3., 4.)];
92        assert!(close(line_interpolate(&single, 0.5), 3., 4.));
93    }
94
95    /// A 3D linestring blends the third ordinate too (the `2 =>` arms
96    /// of the strategy's per-dimension blend).
97    #[test]
98    #[allow(clippy::float_cmp, reason = "midpoint ordinates are exact literals")]
99    fn three_d_midpoint_blends_z() {
100        use geometry_model::Point3D;
101        type P3 = Point3D<f64, Cartesian>;
102        let ls: Linestring<P3> =
103            Linestring::from_vec(alloc::vec![P3::new(0., 0., 0.), P3::new(10., 0., 4.)]);
104        let p = line_interpolate(&ls, 0.5);
105        assert_eq!(p.get::<0>(), 5.0);
106        assert_eq!(p.get::<2>(), 2.0);
107    }
108
109    /// A 4D linestring blends the fourth ordinate (the `3 =>` arms).
110    #[test]
111    #[allow(clippy::float_cmp, reason = "midpoint ordinates are exact literals")]
112    fn four_d_midpoint_blends_all_ordinates() {
113        use geometry_model::Point;
114        use geometry_trait::PointMut as _;
115        type P4 = Point<f64, 4, Cartesian>;
116        let mut a = P4::default();
117        a.set::<3>(8.0);
118        let mut b = P4::default();
119        b.set::<0>(10.0);
120        let ls: Linestring<P4> = Linestring::from_vec(alloc::vec![a, b]);
121        let p = line_interpolate(&ls, 0.5);
122        assert_eq!(p.get::<0>(), 5.0);
123        assert_eq!(p.get::<3>(), 4.0);
124    }
125}