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}