Skip to main content

ifc_spatial/tree/
reference.rs

1//! Elements a spatial structure references without containing them.
2//!
3//! # Containment is not the only link
4//!
5//! `IfcRelContainedInSpatialStructure` places an element in exactly one
6//! structure: `IfcElement.ContainedInStructure` is `SET [0:1]` in IFC2X3 TC1,
7//! IFC4 ADD2 TC1 and IFC4X3 ADD2. `IfcRelReferencedInSpatialStructure`
8//! (IFC2x3 onwards) additionally assigns an element to "those levels of the
9//! project spatial structure, in which they are referenced, but not
10//! primarily contained"; an element "can be referenced to zero, one or
11//! several levels" (IFC4 ADD2 TC1 documentation). The IFC4 example is a
12//! curtain wall contained by the ground floor and referenced by the storeys
13//! above it.
14//!
15//! The two are kept apart: [`SpatialTree::elements_of`] answers containment
16//! only, and [`SpatialTree::referenced_elements`] answers references only.
17//! Merging them would make a multi-storey element look contained twice,
18//! which the schema forbids.
19
20use std::collections::BTreeMap;
21
22use ifc_model::{EntityId, Model};
23
24use super::build::SpatialTree;
25use crate::relation::{link, slots};
26
27impl SpatialTree {
28    /// Record every `IfcRelReferencedInSpatialStructure`.
29    ///
30    /// A relationship naming an absent entity is reported through
31    /// `dangling`, and one whose structure is not a spatial container
32    /// through `anomalies`, as containment reports both.
33    pub(super) fn apply_references(&mut self, model: &Model) {
34        let rel = slots::REFERENCED_IN;
35        for &relationship in model.ids_of_type(rel.type_name) {
36            let Some(structure) = link::refs_in_slot(model, relationship, rel.relating)
37                .into_iter()
38                .next()
39            else {
40                continue;
41            };
42            if model.get(structure).is_none() {
43                self.dangling.push((relationship, structure));
44                continue;
45            }
46            if self.node(structure).is_none() {
47                self.anomalies
48                    .push(super::SpatialAnomaly::ReferencedInNonContainer {
49                        relation: relationship,
50                        structure,
51                    });
52                continue;
53            }
54            for element in link::refs_in_slot(model, relationship, rel.related) {
55                if model.get(element).is_none() {
56                    self.dangling.push((relationship, element));
57                    continue;
58                }
59                push_unique(&mut self.referenced, structure, element);
60                push_unique(&mut self.referenced_in, element, structure);
61            }
62        }
63    }
64
65    /// Elements `IfcRelReferencedInSpatialStructure` references in
66    /// `container`, in file order, each once.
67    ///
68    /// Disjoint in meaning from [`elements_of`](Self::elements_of): a
69    /// referenced element is contained elsewhere (or nowhere). An element
70    /// the file both contains and references in one container appears in
71    /// both lists, because the file states both.
72    #[must_use]
73    pub fn referenced_elements(&self, container: EntityId) -> &[EntityId] {
74        self.referenced.get(&container).map_or(&[], Vec::as_slice)
75    }
76
77    /// Containers that reference `element`, in file order, each once.
78    ///
79    /// Empty for an element that is only contained; its container is
80    /// [`container_of`](Self::container_of).
81    #[must_use]
82    pub fn referencing_structures(&self, element: EntityId) -> &[EntityId] {
83        self.referenced_in.get(&element).map_or(&[], Vec::as_slice)
84    }
85}
86
87/// Append `value` to `key`'s list unless it is already there.
88fn push_unique(map: &mut BTreeMap<EntityId, Vec<EntityId>>, key: EntityId, value: EntityId) {
89    let list = map.entry(key).or_default();
90    if !list.contains(&value) {
91        list.push(value);
92    }
93}