Skip to main content

egml_core/model/geometry/primitives/
shell.rs

1use crate::model::base::HasAssociationAttributes;
2use crate::model::common::{
3    ApplyTransform, ComputeEnvelope, IterGeometries, Triangulate, Triangulation,
4};
5use crate::model::geometry::primitives::{
6    AbstractSurface, AbstractSurfaceProperty, AsAbstractSurface, AsAbstractSurfaceMut,
7    TriangulatedSurface,
8};
9use crate::model::geometry::refs::AbstractGeometryKindRef;
10use crate::model::geometry::{DirectPosition, Envelope};
11use crate::{
12    Error, impl_abstract_surface_mut_traits, impl_abstract_surface_traits, impl_has_geometry_type,
13};
14use nalgebra::{Isometry3, Rotation3, Scale3, Transform3, Vector3};
15use rayon::iter::IntoParallelRefMutIterator;
16use rayon::iter::ParallelIterator;
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct Shell {
20    abstract_surface: AbstractSurface,
21    members: Vec<AbstractSurfaceProperty>,
22}
23
24impl Shell {
25    pub fn new(members: impl IntoIterator<Item = AbstractSurfaceProperty>) -> Result<Self, Error> {
26        let members: Vec<AbstractSurfaceProperty> = members.into_iter().collect();
27        Self::validate(&members)?;
28
29        Ok(Self {
30            abstract_surface: AbstractSurface::default(),
31            members,
32        })
33    }
34
35    pub fn from_abstract_surface(
36        abstract_surface: AbstractSurface,
37        members: impl IntoIterator<Item = AbstractSurfaceProperty>,
38    ) -> Result<Self, Error> {
39        let members: Vec<AbstractSurfaceProperty> = members.into_iter().collect();
40        Self::validate(&members)?;
41
42        Ok(Self {
43            abstract_surface,
44            members,
45        })
46    }
47
48    fn validate(members: &[AbstractSurfaceProperty]) -> Result<(), Error> {
49        if members.is_empty() {
50            return Err(Error::TooFewElements {
51                geometry: "gml:Solid",
52                minimum: 1,
53                spec: Some("OGC 07-036 §10.6.4"),
54                id: None,
55                detail: None,
56            });
57        }
58        Ok(())
59    }
60
61    /// Returns the bounding surface members of this solid.
62    pub fn members(&self) -> &[AbstractSurfaceProperty] {
63        &self.members
64    }
65
66    /// Replaces the bounding surface members.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`Error::TooFewElements`] if `val` is empty.
71    pub fn set_members(&mut self, val: Vec<AbstractSurfaceProperty>) -> Result<(), Error> {
72        Self::validate(&val)?;
73
74        self.members = val;
75        Ok(())
76    }
77
78    pub fn push_member(&mut self, member: AbstractSurfaceProperty) {
79        self.members.push(member);
80    }
81
82    pub fn extend_members(&mut self, members: impl IntoIterator<Item = AbstractSurfaceProperty>) {
83        self.members.extend(members);
84    }
85}
86
87impl AsAbstractSurface for Shell {
88    fn abstract_surface(&self) -> &AbstractSurface {
89        &self.abstract_surface
90    }
91}
92
93impl AsAbstractSurfaceMut for Shell {
94    fn abstract_surface_mut(&mut self) -> &mut AbstractSurface {
95        &mut self.abstract_surface
96    }
97}
98
99impl_abstract_surface_traits!(Shell);
100impl_abstract_surface_mut_traits!(Shell);
101impl_has_geometry_type!(Shell, Shell);
102
103impl IterGeometries for Shell {
104    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
105        Box::new(
106            std::iter::once(self.into()).chain(
107                self.members
108                    .iter()
109                    .filter_map(|x| x.object())
110                    .flat_map(|x| x.iter_geometries()),
111            ),
112        )
113    }
114}
115
116impl Shell {
117    pub fn points(&self) -> Vec<&DirectPosition> {
118        self.members
119            .iter()
120            .flat_map(|x| x.object())
121            .fold(Vec::new(), |mut acc, x| {
122                acc.extend(x.points().iter());
123                acc
124            })
125    }
126
127    /// Returns the total 3D area of all bounding surface members.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`Error::UnresolvedSurfaceReference`] if any member carries only an
132    /// xlink:href that has not been resolved into an inline object.
133    /// Propagates any error from [`AbstractSurfaceKind::area_3d`](crate::model::geometry::primitives::AbstractSurfaceKind::area_3d).
134    pub fn area_3d(&self) -> Result<f64, Error> {
135        self.members
136            .iter()
137            .map(|s| {
138                s.object()
139                    .ok_or_else(|| Error::UnresolvedSurfaceReference {
140                        href: s.href().map(|h| h.to_string()),
141                    })
142                    .and_then(|kind| kind.area_3d())
143            })
144            .collect::<Result<Vec<f64>, Error>>()
145            .map(|area_3ds| area_3ds.into_iter().sum())
146    }
147
148    /// Returns the volume of the closed solid bounded by this shell.
149    ///
150    /// Uses the divergence theorem on the triangulated shell: `V = |Σ a·(b×c)| / 6`
151    /// per triangle. The shell must be closed and watertight for the result to be correct.
152    ///
153    /// # Errors
154    ///
155    /// Propagates any [`Error::TriangulationFailed`] from triangulating the bounding surfaces.
156    pub fn volume_3d(&self) -> Result<f64, Error> {
157        let triangulation = self.triangulate()?;
158        let triangles = triangulation.surface().triangles();
159        let Some(first) = triangles.first() else {
160            return Ok(0.0);
161        };
162
163        // Shift all vertices by the first triangle's vertex to avoid catastrophic
164        // cancellation when world-space coordinates are large (e.g. EPSG:25832).
165        let origin: Vector3<f64> = first.a().into();
166        let signed_vol: f64 = triangles
167            .iter()
168            .map(|t| {
169                let a: Vector3<f64> = Into::<Vector3<f64>>::into(t.a()) - origin;
170                let b: Vector3<f64> = Into::<Vector3<f64>>::into(t.b()) - origin;
171                let c: Vector3<f64> = Into::<Vector3<f64>>::into(t.c()) - origin;
172                a.dot(&b.cross(&c))
173            })
174            .sum();
175        Ok(signed_vol.abs() / 6.0)
176    }
177}
178
179impl ApplyTransform for Shell {
180    fn apply_transform(&mut self, transform: Transform3<f64>) {
181        self.members.par_iter_mut().for_each(|p| {
182            if let Some(x) = p.object_mut() {
183                x.apply_transform(transform);
184            }
185        });
186    }
187
188    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
189        self.members.par_iter_mut().for_each(|p| {
190            if let Some(x) = p.object_mut() {
191                x.apply_isometry(isometry);
192            }
193        });
194    }
195
196    fn apply_translation(&mut self, vector: Vector3<f64>) {
197        self.members.par_iter_mut().for_each(|p| {
198            if let Some(x) = p.object_mut() {
199                x.apply_translation(vector);
200            }
201        });
202    }
203
204    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
205        self.members.par_iter_mut().for_each(|p| {
206            if let Some(x) = p.object_mut() {
207                x.apply_rotation(rotation);
208            }
209        });
210    }
211
212    fn apply_scale(&mut self, scale: Scale3<f64>) {
213        self.members.par_iter_mut().for_each(|p| {
214            if let Some(x) = p.object_mut() {
215                x.apply_scale(scale);
216            }
217        });
218    }
219}
220
221impl ComputeEnvelope for Shell {
222    /// Returns the union of the bounding boxes of all surface members.
223    fn compute_envelope(&self) -> Option<Envelope> {
224        let envelopes: Vec<Envelope> = self
225            .members
226            .iter()
227            .flat_map(|x| x.object())
228            .flat_map(|x| x.compute_envelope())
229            .collect();
230
231        Envelope::from_envelopes(&envelopes)
232    }
233}
234
235impl Triangulate for Shell {
236    /// Members that fail to triangulate individually (e.g. a degenerate ring) are
237    /// skipped rather than failing the whole shell; see their errors via
238    /// [`Triangulation::skipped`].
239    ///
240    /// # Errors
241    ///
242    /// Returns [`Error::TooFewElements`] if no member could be triangulated.
243    fn triangulate(&self) -> Result<Triangulation, Error> {
244        let mut surfaces = Vec::new();
245        let mut skipped = Vec::new();
246
247        for member in self.members.iter().flat_map(|x| x.object()) {
248            match member.triangulate() {
249                Ok(triangulation) => {
250                    let (surface, nested_skipped) = triangulation.into_parts();
251                    surfaces.push(surface);
252                    skipped.extend(nested_skipped);
253                }
254                Err(error) => {
255                    skipped.push(error);
256                }
257            }
258        }
259
260        let combined = TriangulatedSurface::from_triangulated_surfaces(surfaces)?;
261        Ok(Triangulation::new(combined, skipped))
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::model::geometry::primitives::{
269        AbstractRingKind, AbstractSurfaceKind, AbstractSurfacePatchKind, LinearRing, Polygon,
270        PolygonPatch, Surface,
271    };
272
273    fn square(corners: [[f64; 3]; 4]) -> AbstractSurfaceProperty {
274        let ring = LinearRing::new(corners.map(|[x, y, z]| DirectPosition::new(x, y, z).unwrap()))
275            .unwrap();
276        let polygon = Polygon::new(Some(AbstractRingKind::LinearRing(ring)), vec![]).unwrap();
277        AbstractSurfaceProperty::from_object(AbstractSurfaceKind::Polygon(polygon))
278    }
279
280    fn unit_cube_shell() -> Shell {
281        Shell::new([
282            square([[0., 0., 0.], [0., 1., 0.], [1., 1., 0.], [1., 0., 0.]]),
283            square([[0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]]),
284            square([[0., 0., 0.], [1., 0., 0.], [1., 0., 1.], [0., 0., 1.]]),
285            square([[0., 1., 0.], [0., 1., 1.], [1., 1., 1.], [1., 1., 0.]]),
286            square([[0., 0., 0.], [0., 0., 1.], [0., 1., 1.], [0., 1., 0.]]),
287            square([[1., 0., 0.], [1., 1., 0.], [1., 1., 1.], [1., 0., 1.]]),
288        ])
289        .unwrap()
290    }
291
292    #[test]
293    fn area_3d_unit_cube() {
294        assert!((unit_cube_shell().area_3d().unwrap() - 6.0).abs() < 1e-10);
295    }
296
297    #[test]
298    fn area_3d_via_abstract_surface_kind() {
299        let kind = AbstractSurfaceKind::Shell(unit_cube_shell());
300        assert!((kind.area_3d().unwrap() - 6.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn triangulate_skips_member_without_exterior_ring() {
305        let mut shell = unit_cube_shell();
306        let empty = Polygon::new(None, []).unwrap();
307        shell.push_member(AbstractSurfaceProperty::from_object(
308            AbstractSurfaceKind::Polygon(empty),
309        ));
310
311        let triangulation = shell.triangulate().unwrap();
312        assert_eq!(triangulation.skipped(), &[Error::MissingExteriorRing]);
313        assert!((shell.volume_3d().unwrap() - 1.0).abs() < 1e-10);
314    }
315
316    #[test]
317    fn points_with_surface_member() {
318        let ring = LinearRing::new([
319            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
320            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
321            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
322        ])
323        .unwrap();
324        let patch = PolygonPatch::new(Some(AbstractRingKind::LinearRing(ring)), []);
325        let surface = Surface::new(vec![AbstractSurfacePatchKind::PolygonPatch(patch)]);
326        let shell = Shell::new([AbstractSurfaceProperty::from_object(
327            AbstractSurfaceKind::Surface(surface),
328        )])
329        .unwrap();
330        assert_eq!(shell.points().len(), 3);
331    }
332
333    #[test]
334    fn area_3d_unresolved_surface_reference() {
335        let shell = Shell::new([AbstractSurfaceProperty::from_href(
336            "urn:example:surface-1".into(),
337        )])
338        .unwrap();
339        assert_eq!(
340            shell.area_3d(),
341            Err(Error::UnresolvedSurfaceReference {
342                href: Some("urn:example:surface-1".to_string())
343            })
344        );
345    }
346}