Skip to main content

egml_core/model/geometry/primitives/
linear_ring.rs

1use crate::error::Error;
2use crate::model::base::{AsAbstractGml, Id};
3use crate::model::common::{ApplyTransform, ComputeEnvelope, IterGeometries};
4use crate::model::geometry::primitives::{AbstractRing, AsAbstractRing, AsAbstractRingMut};
5use crate::model::geometry::refs::AbstractGeometryKindRef;
6use crate::model::geometry::{DirectPosition, Envelope};
7use crate::{impl_abstract_ring_mut_traits, impl_abstract_ring_traits, impl_has_geometry_type};
8use nalgebra::{Isometry3, Rotation3, Scale3, Transform3, Vector3};
9
10const MINIMUM_NUMBER_OF_POINTS: usize = 3;
11
12/// An implicitly closed ring of at least 3 distinct, non-adjacent positions.
13///
14/// Corresponds to `gml:LinearRing` in [OGC 07-036 §10.5.8](https://docs.ogc.org/is/07-036/07-036.pdf).  The ring is
15/// implicitly closed: the last position is not repeated.
16///
17/// # Invariants
18///
19/// - At least 3 positions.
20/// - No two adjacent positions are equal.
21/// - First and last positions are not equal.
22#[derive(Debug, Clone, PartialEq, Default)]
23pub struct LinearRing {
24    pub abstract_ring: AbstractRing,
25    /// Boxed slice rather than `Vec`: points are written once and then only
26    /// read or transformed in place, so the capacity field would be dead weight.
27    points: Box<[DirectPosition]>,
28}
29
30impl LinearRing {
31    /// Creates a new `LinearRing` from an ordered list of positions.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`Error::TooFewElements`] if `points` has fewer than 3 entries.
36    /// Returns [`Error::AdjacentDuplicatePositions`] if adjacent positions are equal.
37    /// Returns [`Error::RepeatedClosingVertex`] if the first and last positions are equal.
38    pub fn new(points: impl IntoIterator<Item = DirectPosition>) -> Result<Self, Error> {
39        let points: Vec<DirectPosition> = points.into_iter().collect();
40        Self::validate_points(&points, None)?;
41
42        Ok(Self {
43            abstract_ring: AbstractRing::default(),
44            points: points.into_boxed_slice(),
45        })
46    }
47
48    pub fn from_abstract_ring(
49        abstract_ring: AbstractRing,
50        points: impl IntoIterator<Item = DirectPosition>,
51    ) -> Result<Self, Error> {
52        let points: Vec<DirectPosition> = points.into_iter().collect();
53        Self::validate_points(&points, None)?;
54        Ok(Self {
55            abstract_ring,
56            points: points.into_boxed_slice(),
57        })
58    }
59
60    fn validate_points(points: &[DirectPosition], id: Option<&Id>) -> Result<(), Error> {
61        if let Some((index, window)) = points.windows(2).enumerate().find(|(_, w)| w[0] == w[1]) {
62            return Err(Error::AdjacentDuplicatePositions {
63                index,
64                position: window[0],
65            });
66        }
67
68        if points.len() < MINIMUM_NUMBER_OF_POINTS {
69            let detail = if id.is_none() {
70                Some(format!(
71                    "points: {}",
72                    points
73                        .iter()
74                        .map(|p| p.to_string())
75                        .collect::<Vec<String>>()
76                        .join(", ")
77                ))
78            } else {
79                None
80            };
81
82            return Err(Error::TooFewElements {
83                geometry: "gml:LinearRing",
84                minimum: 3,
85                spec: Some("OGC 07-036 §10.5.8"),
86                id: id.cloned(),
87                detail,
88            });
89        }
90
91        let first = *points.first().expect("non-empty validated above");
92        if first == *points.last().expect("non-empty validated above") {
93            return Err(Error::RepeatedClosingVertex { position: first });
94        }
95
96        Ok(())
97    }
98
99    /// Returns the positions of this ring.
100    pub fn points(&self) -> &[DirectPosition] {
101        &self.points
102    }
103
104    /// Replaces the positions of this ring.
105    ///
106    /// # Errors
107    ///
108    /// Returns the same errors as [`new`](Self::new).
109    pub fn set_points(&mut self, val: Vec<DirectPosition>) -> Result<(), Error> {
110        Self::validate_points(&val, self.id())?;
111
112        self.points = val.into_boxed_slice();
113        Ok(())
114    }
115}
116
117impl AsAbstractRing for LinearRing {
118    fn abstract_ring(&self) -> &AbstractRing {
119        &self.abstract_ring
120    }
121}
122
123impl AsAbstractRingMut for LinearRing {
124    fn abstract_ring_mut(&mut self) -> &mut AbstractRing {
125        &mut self.abstract_ring
126    }
127}
128
129impl_abstract_ring_traits!(LinearRing);
130impl_abstract_ring_mut_traits!(LinearRing);
131impl_has_geometry_type!(LinearRing, LinearRing);
132
133impl LinearRing {
134    /// Returns the 3D perimeter of this ring.
135    ///
136    /// Sums the distances between consecutive vertices, including the implicit
137    /// closing segment from the last vertex back to the first.
138    pub fn length_3d(&self) -> f64 {
139        let n = self.points.len();
140        (0..n)
141            .map(|i| {
142                let vi: Vector3<f64> = self.points[i].into();
143                let vj: Vector3<f64> = self.points[(i + 1) % n].into();
144                (vj - vi).norm()
145            })
146            .sum()
147    }
148
149    /// Returns the 3D area_3d of this ring using the cross-product summation formula.
150    ///
151    /// Computes `|Σ (vᵢ × vᵢ₊₁)| / 2` over all consecutive vertex pairs (with wrap-around),
152    /// which gives the correct planar area_3d regardless of orientation in 3D space.
153    pub fn area_3d(&self) -> f64 {
154        let n = self.points.len();
155        let mut cross_sum = Vector3::zeros();
156        for i in 0..n {
157            let vi: Vector3<f64> = self.points[i].into();
158            let vj: Vector3<f64> = self.points[(i + 1) % n].into();
159            cross_sum += vi.cross(&vj);
160        }
161        cross_sum.norm() * 0.5
162    }
163}
164
165impl ApplyTransform for LinearRing {
166    fn apply_transform(&mut self, transform: Transform3<f64>) {
167        self.points.iter_mut().for_each(|p| {
168            p.apply_transform(transform);
169        });
170    }
171
172    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
173        self.points.iter_mut().for_each(|p| {
174            p.apply_isometry(isometry);
175        });
176    }
177
178    fn apply_translation(&mut self, vector: Vector3<f64>) {
179        self.points.iter_mut().for_each(|p| {
180            p.apply_translation(vector);
181        });
182    }
183
184    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
185        self.points.iter_mut().for_each(|p| {
186            p.apply_rotation(rotation);
187        });
188    }
189
190    fn apply_scale(&mut self, scale: Scale3<f64>) {
191        self.points.iter_mut().for_each(|p| {
192            p.apply_scale(scale);
193        });
194    }
195}
196
197impl ComputeEnvelope for LinearRing {
198    /// Returns the axis-aligned bounding box of all positions in this ring.
199    fn compute_envelope(&self) -> Option<Envelope> {
200        Some(Envelope::from_points(&self.points).expect("linear ring must have valid points"))
201    }
202}
203
204impl IterGeometries for LinearRing {
205    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
206        Box::new(std::iter::once(self.into()))
207    }
208}
209
210#[cfg(test)]
211mod test {
212    use super::*;
213    use nalgebra::{Isometry3, Vector3};
214
215    #[test]
216    fn length_3d_unit_square_includes_closing_segment() {
217        let ring = LinearRing::new([
218            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
219            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
220            DirectPosition::new(1.0, 1.0, 0.0).unwrap(),
221            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
222        ])
223        .unwrap();
224        assert!((ring.length_3d() - 4.0).abs() < 1e-10);
225    }
226
227    #[test]
228    fn length_3d_right_triangle_in_3d() {
229        // 3-4-5 right triangle in the XZ plane — perimeter 12.
230        let ring = LinearRing::new([
231            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
232            DirectPosition::new(3.0, 0.0, 0.0).unwrap(),
233            DirectPosition::new(3.0, 0.0, 4.0).unwrap(),
234        ])
235        .unwrap();
236        assert!((ring.length_3d() - 12.0).abs() < 1e-10);
237    }
238
239    #[test]
240    fn area_3d_unit_square_xy() {
241        let ring = LinearRing::new([
242            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
243            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
244            DirectPosition::new(1.0, 1.0, 0.0).unwrap(),
245            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
246        ])
247        .unwrap();
248        assert!((ring.area_3d() - 1.0).abs() < 1e-10);
249    }
250
251    #[test]
252    fn area_3d_tilted_rectangle() {
253        // Rectangle with sides 1 and sqrt(5) tilted in 3D — area_3d should be sqrt(5).
254        let ring = LinearRing::new([
255            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
256            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
257            DirectPosition::new(1.0, 1.0, 2.0).unwrap(),
258            DirectPosition::new(0.0, 1.0, 2.0).unwrap(),
259        ])
260        .unwrap();
261        let expected = 5.0_f64.sqrt();
262        assert!((ring.area_3d() - expected).abs() < 1e-10);
263    }
264
265    #[test]
266    fn area_3d_triangle() {
267        // Right triangle with legs 3 and 4 — area_3d should be 6.
268        let ring = LinearRing::new([
269            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
270            DirectPosition::new(3.0, 0.0, 0.0).unwrap(),
271            DirectPosition::new(0.0, 4.0, 0.0).unwrap(),
272        ])
273        .unwrap();
274        assert!((ring.area_3d() - 6.0).abs() < 1e-10);
275    }
276
277    #[test]
278    fn linear_ring_construction_test() {
279        let points = vec![
280            DirectPosition::new(601.92791444745251, 1130.4631113024607, 9.0130903915382347)
281                .unwrap(),
282            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
283                .unwrap(),
284            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
285                .unwrap(),
286        ];
287        let result = LinearRing::new(points);
288
289        assert!(matches!(
290            result,
291            Err(Error::AdjacentDuplicatePositions { .. })
292        ));
293    }
294
295    #[test]
296    fn offset_linear_ring_test() {
297        let mut linear_ring = LinearRing::new([
298            DirectPosition::new(1.0, 2.0, 3.0).unwrap(),
299            DirectPosition::new(2.0, 4.0, 6.0).unwrap(),
300            DirectPosition::new(4.0, 7.0, 9.0).unwrap(),
301        ])
302        .unwrap();
303        //let offset = nalgebra::Vector3::<f64>::new(1.0, -1.0, 3.0);
304        let isometry: Isometry3<f64> =
305            Isometry3::new(Vector3::new(1.0, -1.0, 3.0), Default::default());
306        let expected_linear_ring = LinearRing::new([
307            DirectPosition::new(2.0, 1.0, 6.0).unwrap(),
308            DirectPosition::new(3.0, 3.0, 9.0).unwrap(),
309            DirectPosition::new(5.0, 6.0, 12.0).unwrap(),
310        ])
311        .unwrap();
312
313        linear_ring.apply_isometry(isometry);
314
315        assert_eq!(linear_ring, expected_linear_ring);
316    }
317}