Skip to main content

egml_core/model/geometry/primitives/
line_string.rs

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