Skip to main content

egml_core/model/geometry/primitives/
linear_ring.rs

1use crate::error::Error;
2use crate::impl_abstract_ring_traits;
3use crate::model::base::{AsAbstractGml, Id};
4use crate::model::geometry::primitives::{AbstractRing, AsAbstractRing, AsAbstractRingMut};
5use crate::model::geometry::{DirectPosition, Envelope};
6use nalgebra::{Isometry3, Vector3};
7
8const MINIMUM_NUMBER_OF_POINTS: usize = 3;
9
10/// An implicitly closed ring of at least 3 distinct, non-adjacent positions.
11///
12/// Corresponds to `gml:LinearRing` in [OGC 07-036 §10.5.8](https://docs.ogc.org/is/07-036/07-036.pdf).  The ring is
13/// implicitly closed: the last position is not repeated.
14///
15/// # Invariants
16///
17/// - At least 3 positions.
18/// - No two adjacent positions are equal.
19/// - First and last positions are not equal.
20#[derive(Debug, Clone, PartialEq, Default)]
21pub struct LinearRing {
22    pub(crate) abstract_ring: AbstractRing,
23    points: Vec<DirectPosition>,
24}
25
26impl LinearRing {
27    /// Creates a new `LinearRing` from an ordered list of positions.
28    ///
29    /// # Errors
30    ///
31    /// Returns [`Error::TooFewElements`] if `points` has fewer than 3 entries.
32    /// Returns [`Error::AdjacentDuplicatePositions`] if adjacent positions are equal.
33    /// Returns [`Error::RepeatedClosingVertex`] if the first and last positions are equal.
34    pub fn new(points: impl IntoIterator<Item = DirectPosition>) -> Result<Self, Error> {
35        let points: Vec<DirectPosition> = points.into_iter().collect();
36        Self::validate_points(&points, None)?;
37        Ok(Self {
38            abstract_ring: AbstractRing::default(),
39            points,
40        })
41    }
42
43    /// Returns the positions of this ring.
44    pub fn points(&self) -> &[DirectPosition] {
45        &self.points
46    }
47
48    /// Replaces the positions of this ring.
49    ///
50    /// # Errors
51    ///
52    /// Returns the same errors as [`new`](Self::new).
53    pub fn set_points(&mut self, val: Vec<DirectPosition>) -> Result<(), Error> {
54        Self::validate_points(&val, self.id())?;
55        self.points = val;
56        Ok(())
57    }
58}
59
60impl LinearRing {
61    /// Applies a rigid-body transform to all positions in place.
62    pub fn apply_transform(&mut self, m: &Isometry3<f64>) {
63        self.points.iter_mut().for_each(|p| {
64            p.apply_transform(m);
65        });
66    }
67
68    /// Returns the 3D area_3d of this ring using the cross-product summation formula.
69    ///
70    /// Computes `|Σ (vᵢ × vᵢ₊₁)| / 2` over all consecutive vertex pairs (with wrap-around),
71    /// which gives the correct planar area_3d regardless of orientation in 3D space.
72    pub fn area_3d(&self) -> f64 {
73        let n = self.points.len();
74        let mut cross_sum = Vector3::zeros();
75        for i in 0..n {
76            let vi: Vector3<f64> = self.points[i].into();
77            let vj: Vector3<f64> = self.points[(i + 1) % n].into();
78            cross_sum += vi.cross(&vj);
79        }
80        cross_sum.norm() * 0.5
81    }
82
83    /// Returns the axis-aligned bounding box of all positions in this ring.
84    pub fn compute_envelope(&self) -> Envelope {
85        Envelope::from_points(&self.points).expect("linear ring must have valid points")
86    }
87
88    fn validate_points(points: &[DirectPosition], id: Option<&Id>) -> Result<(), Error> {
89        if let Some((index, window)) = points.windows(2).enumerate().find(|(_, w)| w[0] == w[1]) {
90            return Err(Error::AdjacentDuplicatePositions {
91                index,
92                position: window[0],
93            });
94        }
95        if points.len() < MINIMUM_NUMBER_OF_POINTS {
96            let detail = if id.is_none() {
97                Some(format!(
98                    "points: {}",
99                    points
100                        .iter()
101                        .map(|p| p.to_string())
102                        .collect::<Vec<String>>()
103                        .join(", ")
104                ))
105            } else {
106                None
107            };
108
109            return Err(Error::TooFewElements {
110                geometry: "gml:LinearRing",
111                minimum: 3,
112                spec: Some("OGC 07-036 §10.5.8"),
113                id: id.cloned(),
114                detail,
115            });
116        }
117        let first = *points.first().expect("non-empty validated above");
118        if first == *points.last().expect("non-empty validated above") {
119            return Err(Error::RepeatedClosingVertex { position: first });
120        }
121        Ok(())
122    }
123}
124
125impl AsAbstractRing for LinearRing {
126    fn abstract_ring(&self) -> &AbstractRing {
127        &self.abstract_ring
128    }
129}
130
131impl AsAbstractRingMut for LinearRing {
132    fn abstract_ring_mut(&mut self) -> &mut AbstractRing {
133        &mut self.abstract_ring
134    }
135}
136
137impl_abstract_ring_traits!(LinearRing);
138
139#[cfg(test)]
140mod test {
141    use super::*;
142    use nalgebra::Vector3;
143
144    #[test]
145    fn area_3d_unit_square_xy() {
146        let ring = LinearRing::new([
147            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
148            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
149            DirectPosition::new(1.0, 1.0, 0.0).unwrap(),
150            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
151        ])
152        .unwrap();
153        assert!((ring.area_3d() - 1.0).abs() < 1e-10);
154    }
155
156    #[test]
157    fn area_3d_tilted_rectangle() {
158        // Rectangle with sides 1 and sqrt(5) tilted in 3D — area_3d should be sqrt(5).
159        let ring = LinearRing::new([
160            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
161            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
162            DirectPosition::new(1.0, 1.0, 2.0).unwrap(),
163            DirectPosition::new(0.0, 1.0, 2.0).unwrap(),
164        ])
165        .unwrap();
166        let expected = 5.0_f64.sqrt();
167        assert!((ring.area_3d() - expected).abs() < 1e-10);
168    }
169
170    #[test]
171    fn area_3d_triangle() {
172        // Right triangle with legs 3 and 4 — area_3d should be 6.
173        let ring = LinearRing::new([
174            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
175            DirectPosition::new(3.0, 0.0, 0.0).unwrap(),
176            DirectPosition::new(0.0, 4.0, 0.0).unwrap(),
177        ])
178        .unwrap();
179        assert!((ring.area_3d() - 6.0).abs() < 1e-10);
180    }
181
182    #[test]
183    fn linear_ring_construction_test() {
184        let points = vec![
185            DirectPosition::new(601.92791444745251, 1130.4631113024607, 9.0130903915382347)
186                .unwrap(),
187            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
188                .unwrap(),
189            DirectPosition::new(601.92791832847342, 1130.4631032795705, 9.0130907233102739)
190                .unwrap(),
191        ];
192        let result = LinearRing::new(points);
193
194        assert!(matches!(
195            result,
196            Err(Error::AdjacentDuplicatePositions { .. })
197        ));
198    }
199
200    #[test]
201    fn offset_linear_ring_test() {
202        let mut linear_ring = LinearRing::new([
203            DirectPosition::new(1.0, 2.0, 3.0).unwrap(),
204            DirectPosition::new(2.0, 4.0, 6.0).unwrap(),
205            DirectPosition::new(4.0, 7.0, 9.0).unwrap(),
206        ])
207        .unwrap();
208        //let offset = nalgebra::Vector3::<f64>::new(1.0, -1.0, 3.0);
209        let isometry: Isometry3<f64> =
210            Isometry3::new(Vector3::new(1.0, -1.0, 3.0), Default::default());
211        let expected_linear_ring = LinearRing::new([
212            DirectPosition::new(2.0, 1.0, 6.0).unwrap(),
213            DirectPosition::new(3.0, 3.0, 9.0).unwrap(),
214            DirectPosition::new(5.0, 6.0, 12.0).unwrap(),
215        ])
216        .unwrap();
217
218        linear_ring.apply_transform(&isometry);
219
220        assert_eq!(linear_ring, expected_linear_ring);
221    }
222}