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