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    points: Vec<DirectPosition>,
26}
27
28impl LinearRing {
29    /// Creates a new `LinearRing` from an ordered list of positions.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`Error::TooFewElements`] if `points` has fewer than 3 entries.
34    /// Returns [`Error::AdjacentDuplicatePositions`] if adjacent positions are equal.
35    /// Returns [`Error::RepeatedClosingVertex`] if the first and last positions are equal.
36    pub fn new(points: impl IntoIterator<Item = DirectPosition>) -> Result<Self, Error> {
37        let points: Vec<DirectPosition> = points.into_iter().collect();
38        Self::validate_points(&points, None)?;
39
40        Ok(Self {
41            abstract_ring: AbstractRing::default(),
42            points,
43        })
44    }
45
46    pub fn from_abstract_ring(
47        abstract_ring: AbstractRing,
48        points: impl IntoIterator<Item = DirectPosition>,
49    ) -> Result<Self, Error> {
50        let points: Vec<DirectPosition> = points.into_iter().collect();
51        Self::validate_points(&points, None)?;
52        Ok(Self {
53            abstract_ring,
54            points,
55        })
56    }
57
58    fn validate_points(points: &[DirectPosition], id: Option<&Id>) -> Result<(), Error> {
59        if let Some((index, window)) = points.windows(2).enumerate().find(|(_, w)| w[0] == w[1]) {
60            return Err(Error::AdjacentDuplicatePositions {
61                index,
62                position: window[0],
63            });
64        }
65
66        if points.len() < MINIMUM_NUMBER_OF_POINTS {
67            let detail = if id.is_none() {
68                Some(format!(
69                    "points: {}",
70                    points
71                        .iter()
72                        .map(|p| p.to_string())
73                        .collect::<Vec<String>>()
74                        .join(", ")
75                ))
76            } else {
77                None
78            };
79
80            return Err(Error::TooFewElements {
81                geometry: "gml:LinearRing",
82                minimum: 3,
83                spec: Some("OGC 07-036 §10.5.8"),
84                id: id.cloned(),
85                detail,
86            });
87        }
88
89        let first = *points.first().expect("non-empty validated above");
90        if first == *points.last().expect("non-empty validated above") {
91            return Err(Error::RepeatedClosingVertex { position: first });
92        }
93
94        Ok(())
95    }
96
97    /// Returns the positions of this ring.
98    pub fn points(&self) -> &[DirectPosition] {
99        &self.points
100    }
101
102    /// Replaces the positions of this ring.
103    ///
104    /// # Errors
105    ///
106    /// Returns the same errors as [`new`](Self::new).
107    pub fn set_points(&mut self, val: Vec<DirectPosition>) -> Result<(), Error> {
108        Self::validate_points(&val, self.id())?;
109
110        self.points = val;
111        Ok(())
112    }
113}
114
115impl AsAbstractRing for LinearRing {
116    fn abstract_ring(&self) -> &AbstractRing {
117        &self.abstract_ring
118    }
119}
120
121impl AsAbstractRingMut for LinearRing {
122    fn abstract_ring_mut(&mut self) -> &mut AbstractRing {
123        &mut self.abstract_ring
124    }
125}
126
127impl_abstract_ring_traits!(LinearRing);
128impl_abstract_ring_mut_traits!(LinearRing);
129impl_has_geometry_type!(LinearRing, LinearRing);
130
131impl LinearRing {
132    pub fn length_3d(&self) -> f64 {
133        todo!("needs to be implemented for LinearRing")
134    }
135
136    /// Returns the 3D area_3d of this ring using the cross-product summation formula.
137    ///
138    /// Computes `|Σ (vᵢ × vᵢ₊₁)| / 2` over all consecutive vertex pairs (with wrap-around),
139    /// which gives the correct planar area_3d regardless of orientation in 3D space.
140    pub fn area_3d(&self) -> f64 {
141        let n = self.points.len();
142        let mut cross_sum = Vector3::zeros();
143        for i in 0..n {
144            let vi: Vector3<f64> = self.points[i].into();
145            let vj: Vector3<f64> = self.points[(i + 1) % n].into();
146            cross_sum += vi.cross(&vj);
147        }
148        cross_sum.norm() * 0.5
149    }
150}
151
152impl ApplyTransform for LinearRing {
153    fn apply_transform(&mut self, transform: Transform3<f64>) {
154        self.points.iter_mut().for_each(|p| {
155            p.apply_transform(transform);
156        });
157    }
158
159    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
160        self.points.iter_mut().for_each(|p| {
161            p.apply_isometry(isometry);
162        });
163    }
164
165    fn apply_translation(&mut self, vector: Vector3<f64>) {
166        self.points.iter_mut().for_each(|p| {
167            p.apply_translation(vector);
168        });
169    }
170
171    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
172        self.points.iter_mut().for_each(|p| {
173            p.apply_rotation(rotation);
174        });
175    }
176
177    fn apply_scale(&mut self, scale: Scale3<f64>) {
178        self.points.iter_mut().for_each(|p| {
179            p.apply_scale(scale);
180        });
181    }
182}
183
184impl ComputeEnvelope for LinearRing {
185    /// Returns the axis-aligned bounding box of all positions in this ring.
186    fn compute_envelope(&self) -> Option<Envelope> {
187        Some(Envelope::from_points(&self.points).expect("linear ring must have valid points"))
188    }
189}
190
191impl IterGeometries for LinearRing {
192    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
193        Box::new(std::iter::once(self.into()))
194    }
195}
196
197#[cfg(test)]
198mod test {
199    use super::*;
200    use nalgebra::{Isometry3, Vector3};
201
202    #[test]
203    fn area_3d_unit_square_xy() {
204        let ring = LinearRing::new([
205            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
206            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
207            DirectPosition::new(1.0, 1.0, 0.0).unwrap(),
208            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
209        ])
210        .unwrap();
211        assert!((ring.area_3d() - 1.0).abs() < 1e-10);
212    }
213
214    #[test]
215    fn area_3d_tilted_rectangle() {
216        // Rectangle with sides 1 and sqrt(5) tilted in 3D — area_3d should be sqrt(5).
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, 2.0).unwrap(),
221            DirectPosition::new(0.0, 1.0, 2.0).unwrap(),
222        ])
223        .unwrap();
224        let expected = 5.0_f64.sqrt();
225        assert!((ring.area_3d() - expected).abs() < 1e-10);
226    }
227
228    #[test]
229    fn area_3d_triangle() {
230        // Right triangle with legs 3 and 4 — area_3d should be 6.
231        let ring = LinearRing::new([
232            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
233            DirectPosition::new(3.0, 0.0, 0.0).unwrap(),
234            DirectPosition::new(0.0, 4.0, 0.0).unwrap(),
235        ])
236        .unwrap();
237        assert!((ring.area_3d() - 6.0).abs() < 1e-10);
238    }
239
240    #[test]
241    fn linear_ring_construction_test() {
242        let points = vec![
243            DirectPosition::new(601.92791444745251, 1130.4631113024607, 9.0130903915382347)
244                .unwrap(),
245            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
246                .unwrap(),
247            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
248                .unwrap(),
249        ];
250        let result = LinearRing::new(points);
251
252        assert!(matches!(
253            result,
254            Err(Error::AdjacentDuplicatePositions { .. })
255        ));
256    }
257
258    #[test]
259    fn offset_linear_ring_test() {
260        let mut linear_ring = LinearRing::new([
261            DirectPosition::new(1.0, 2.0, 3.0).unwrap(),
262            DirectPosition::new(2.0, 4.0, 6.0).unwrap(),
263            DirectPosition::new(4.0, 7.0, 9.0).unwrap(),
264        ])
265        .unwrap();
266        //let offset = nalgebra::Vector3::<f64>::new(1.0, -1.0, 3.0);
267        let isometry: Isometry3<f64> =
268            Isometry3::new(Vector3::new(1.0, -1.0, 3.0), Default::default());
269        let expected_linear_ring = LinearRing::new([
270            DirectPosition::new(2.0, 1.0, 6.0).unwrap(),
271            DirectPosition::new(3.0, 3.0, 9.0).unwrap(),
272            DirectPosition::new(5.0, 6.0, 12.0).unwrap(),
273        ])
274        .unwrap();
275
276        linear_ring.apply_isometry(isometry);
277
278        assert_eq!(linear_ring, expected_linear_ring);
279    }
280}