Skip to main content

egml_core/model/geometry/primitives/
line_string.rs

1use crate::model::common::{ApplyTransform, ComputeEnvelope, IterGeometries};
2use crate::model::geometry::primitives::{AbstractCurve, AsAbstractCurve, AsAbstractCurveMut};
3use crate::model::geometry::refs::AbstractGeometryKindRef;
4use crate::model::geometry::{DirectPosition, Envelope};
5use crate::{
6    Error, impl_abstract_curve_mut_traits, impl_abstract_curve_traits, impl_has_geometry_type,
7};
8use nalgebra::{Isometry3, Rotation3, Scale3, Transform3, Vector3};
9
10/// An ordered sequence of two or more coordinate positions forming a 1-D curve.
11///
12/// Corresponds to `gml:LineString` in [OGC 07-036 §10.4.4](https://docs.ogc.org/is/07-036/07-036.pdf).
13#[derive(Debug, Clone, PartialEq, Default)]
14pub struct LineString {
15    pub abstract_curve: AbstractCurve,
16    points: Vec<DirectPosition>,
17}
18
19impl LineString {
20    /// Creates a new `LineString` from an ordered list of positions.
21    ///
22    /// # Errors
23    ///
24    /// Returns [`Error::TooFewElements`] if `points` contains fewer than 2 entries.
25    /// Returns [`Error::AdjacentDuplicatePositions`] if adjacent positions are equal.
26    pub fn new(points: impl IntoIterator<Item = DirectPosition>) -> Result<Self, Error> {
27        let points: Vec<DirectPosition> = points.into_iter().collect();
28        Self::validate(&points)?;
29
30        Ok(Self {
31            abstract_curve: AbstractCurve::default(),
32            points,
33        })
34    }
35
36    pub fn from_abstract_curve(
37        abstract_curve: AbstractCurve,
38        points: impl IntoIterator<Item = DirectPosition>,
39    ) -> Result<Self, Error> {
40        let points: Vec<DirectPosition> = points.into_iter().collect();
41        Self::validate(&points)?;
42
43        Ok(Self {
44            abstract_curve,
45            points,
46        })
47    }
48
49    fn validate(points: &[DirectPosition]) -> Result<(), Error> {
50        if let Some((index, window)) = points.windows(2).enumerate().find(|(_, w)| w[0] == w[1]) {
51            return Err(Error::AdjacentDuplicatePositions {
52                index,
53                position: window[0],
54            });
55        }
56
57        if points.len() < 2 {
58            return Err(Error::TooFewElements {
59                geometry: "gml:LineString",
60                minimum: 2,
61                spec: Some("OGC 07-036 §10.4.4"),
62                id: None,
63                detail: None,
64            });
65        }
66
67        Ok(())
68    }
69
70    /// Returns the ordered positions of this line string.
71    pub fn points(&self) -> &[DirectPosition] {
72        &self.points
73    }
74
75    /// Replaces the positions of this line string.
76    ///
77    /// # Errors
78    ///
79    /// Returns the same errors as [`new`](Self::new).
80    pub fn set_points(
81        &mut self,
82        points: impl IntoIterator<Item = DirectPosition>,
83    ) -> Result<(), Error> {
84        let points: Vec<DirectPosition> = points.into_iter().collect();
85        Self::validate(&points)?;
86        self.points = points;
87        Ok(())
88    }
89}
90
91impl AsAbstractCurve for LineString {
92    fn abstract_curve(&self) -> &AbstractCurve {
93        &self.abstract_curve
94    }
95}
96
97impl AsAbstractCurveMut for LineString {
98    fn abstract_curve_mut(&mut self) -> &mut AbstractCurve {
99        &mut self.abstract_curve
100    }
101}
102
103impl_abstract_curve_traits!(LineString);
104impl_abstract_curve_mut_traits!(LineString);
105impl_has_geometry_type!(LineString, LineString);
106
107impl LineString {
108    /// Returns the total 3D length as the sum of Euclidean distances between consecutive points.
109    pub fn length_3d(&self) -> f64 {
110        self.points
111            .windows(2)
112            .map(|w| {
113                let a: Vector3<f64> = w[0].into();
114                let b: Vector3<f64> = w[1].into();
115                (b - a).norm()
116            })
117            .sum()
118    }
119}
120
121impl ApplyTransform for LineString {
122    fn apply_transform(&mut self, transform: Transform3<f64>) {
123        self.points.iter_mut().for_each(|p| {
124            p.apply_transform(transform);
125        });
126    }
127
128    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
129        self.points.iter_mut().for_each(|p| {
130            p.apply_isometry(isometry);
131        });
132    }
133
134    fn apply_translation(&mut self, vector: Vector3<f64>) {
135        self.points.iter_mut().for_each(|p| {
136            p.apply_translation(vector);
137        });
138    }
139
140    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
141        self.points.iter_mut().for_each(|p| {
142            p.apply_rotation(rotation);
143        });
144    }
145
146    fn apply_scale(&mut self, scale: Scale3<f64>) {
147        self.points.iter_mut().for_each(|p| {
148            p.apply_scale(scale);
149        });
150    }
151}
152
153impl ComputeEnvelope for LineString {
154    fn compute_envelope(&self) -> Option<Envelope> {
155        Some(Envelope::from_points(&self.points).expect("line string must have valid points"))
156    }
157}
158
159impl IterGeometries for LineString {
160    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
161        Box::new(std::iter::once(self.into()))
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn length_3d_axis_aligned() {
171        let ls = LineString::new([
172            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
173            DirectPosition::new(3.0, 0.0, 0.0).unwrap(),
174            DirectPosition::new(3.0, 4.0, 0.0).unwrap(),
175        ])
176        .unwrap();
177        assert!((ls.length_3d() - 7.0).abs() < 1e-10);
178    }
179
180    #[test]
181    fn length_3d_diagonal() {
182        // Single segment along the space diagonal of a unit cube — length sqrt(3).
183        let ls = LineString::new([
184            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
185            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
186        ])
187        .unwrap();
188        let expected = 3.0_f64.sqrt();
189        assert!((ls.length_3d() - expected).abs() < 1e-10);
190    }
191}