gistools/geometry/tools/lines/
along.rs

1use crate::geometry::{bearing, destination};
2use libm::hypot;
3use s2json::{
4    Feature, Features, Geometry, GetXY, LineString, LineStringGeometry, NewXY, VectorFeature,
5    VectorGeometry, VectorLineString, VectorLineStringGeometry,
6};
7
8// TODO: If GetZ returns a value, treat each point as a 3D point. Then we can support 3DGeometry
9
10/// Given a linestring in degrees and a distance, create a Point along the line
11///
12/// If no radius is provided, defaults to the Earth's radius
13///
14/// NOTE: If your feature/geometry isn't a line, the point returned will have an x and y of [`f64::NAN`]
15///
16/// This trait is implemented for:
17/// - [`Feature`]
18/// - [`Geometry`]
19/// - [`LineStringGeometry`]
20/// - [`LineString`]
21/// - [`VectorFeature`]
22/// - [`VectorGeometry`]
23/// - [`VectorLineStringGeometry`]
24/// - [`VectorLineString`]
25/// - [`Features`]
26/// - `&[P]` where P implements [`GetXY`] and [`GetZ`]
27///
28/// And all specific geometries of the above enums
29///
30/// If you want to work with the function directly use [`along_line`]
31pub trait Along<P: NewXY> {
32    /// Get the total euclidean distance of a line or lines
33    fn along_line(&self, distance: f64, radius: Option<f64>) -> P;
34}
35
36// Relative Trait
37
38impl<P: GetXY + NewXY, Q: GetXY> Along<P> for &[Q] {
39    fn along_line(&self, distance: f64, radius: Option<f64>) -> P {
40        along_line(self, distance, radius)
41    }
42}
43
44// Features
45
46impl<M, P: Clone + Default, D: Clone + Default, Q: NewXY> Along<Q> for Feature<M, P, D> {
47    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
48        self.geometry.along_line(distance, radius)
49    }
50}
51impl<M: Clone + Default, Q: NewXY> Along<Q> for Geometry<M> {
52    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
53        match self {
54            Geometry::LineString(g) => g.along_line(distance, radius),
55            _ => Q::new_xy(f64::NAN, f64::NAN),
56        }
57    }
58}
59impl<M: Clone + Default, Q: NewXY> Along<Q> for LineStringGeometry<M> {
60    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
61        self.coordinates.along_line(distance, radius)
62    }
63}
64impl<Q: NewXY> Along<Q> for LineString {
65    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
66        along_line(self, distance, radius)
67    }
68}
69
70// Vector Features
71
72impl<M, P: Clone + Default, D: Clone + Default, Q: NewXY> Along<Q> for VectorFeature<M, P, D> {
73    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
74        self.geometry.along_line(distance, radius)
75    }
76}
77impl<M: Clone + Default, Q: NewXY> Along<Q> for VectorGeometry<M> {
78    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
79        match self {
80            VectorGeometry::LineString(g) => g.along_line(distance, radius),
81            _ => Q::new_xy(f64::NAN, f64::NAN),
82        }
83    }
84}
85impl<M: Clone + Default, Q: NewXY> Along<Q> for VectorLineStringGeometry<M> {
86    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
87        self.coordinates.along_line(distance, radius)
88    }
89}
90impl<M: Clone + Default, Q: NewXY> Along<Q> for VectorLineString<M> {
91    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
92        along_line(self, distance, radius)
93    }
94}
95
96// Features
97
98impl<M, P: Clone + Default, D: Clone + Default, Q: NewXY> Along<Q> for Features<M, P, D> {
99    fn along_line(&self, distance: f64, radius: Option<f64>) -> Q {
100        match self {
101            Features::Feature(f) => f.along_line(distance, radius),
102            Features::VectorFeature(f) => f.along_line(distance, radius),
103        }
104    }
105}
106
107/// Given a linestring in degrees and a distance, create a Point along the line
108///
109/// If no radius is provided, defaults to the Earth's radius
110pub fn along_line<P: GetXY, Q: NewXY>(coords: &[P], distance: f64, radius: Option<f64>) -> Q {
111    let mut travelled = 0.;
112    for i in 0..coords.len() {
113        if distance >= travelled && i == coords.len() - 1 {
114            break;
115        } else if travelled >= distance {
116            let overshot = distance - travelled;
117            if overshot == 0. {
118                return Q::new_xy(coords[i].x(), coords[i].y());
119            } else {
120                let direction = bearing(&coords[i], &coords[i - 1]) - 180.;
121                let interpolated = destination(&coords[i], overshot, direction, radius);
122                return interpolated;
123            }
124        } else {
125            travelled +=
126                hypot(coords[i + 1].x() - coords[i].x(), coords[i + 1].y() - coords[i].y());
127        }
128    }
129    let last = coords.len() - 1;
130    Q::new_xy(coords[last].x(), coords[last].y())
131}