ifc_systems/zone/spatial_group.rs
1//! Where an element sits spatially, and where it is merely referenced.
2//!
3//! # Two relationships that are not interchangeable
4//!
5//! ```text
6//! IfcRelContainedInSpatialStructure 4 = RelatedElements 5 = RelatingStructure
7//! IfcRelReferencedInSpatialStructure 4 = RelatedElements 5 = RelatingStructure
8//! ```
9//!
10//! Same slot layout, different cardinality and different meaning:
11//!
12//! - `ContainedInStructure` is `SET [0:1]`: an element has AT MOST ONE
13//! containing structure. This is its home.
14//! - `ReferencedInStructures` is `SET [0:?]`: an element may be referenced by
15//! MANY structures. A duct passing through five rooms is referenced by all
16//! five and contained by none of them.
17//!
18//! Merging the two loses the distinction that makes the second useful. A
19//! caller asking "which storey owns this pump" wants containment; a caller
20//! asking "which rooms does this duct serve" wants references. They are kept
21//! apart.
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use ifc_model::{EntityId, Model, Value};
26
27use crate::error::SystemAnomaly;
28
29fn refs(value: Option<&Value>) -> Vec<EntityId> {
30 match value {
31 Some(Value::List(items)) => items
32 .iter()
33 .filter_map(|v| match v {
34 Value::Ref(id) => Some(*id),
35 _ => None,
36 })
37 .collect(),
38 Some(Value::Ref(id)) => vec![*id],
39 _ => Vec::new(),
40 }
41}
42
43pub(crate) mod slot {
44 /// Both spatial relationships put elements at 4 and the structure at 5.
45 pub const RELATED_ELEMENTS: usize = 4;
46 pub const RELATING_STRUCTURE: usize = 5;
47}
48
49/// Where one element sits in the spatial structure.
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
51pub struct SpatialPlacement {
52 /// The single containing structure, if the file states one.
53 ///
54 /// `Option`, not `Vec`, because the schema says `SET [0:1]`. A file
55 /// stating two is malformed and reports `ContainedTwice`.
56 pub contained_in: Option<EntityId>,
57 /// Structures that reference this element, ascending by id.
58 ///
59 /// Unbounded by the schema, so a `Vec` is the honest shape.
60 pub referenced_in: Vec<EntityId>,
61}
62
63impl SpatialPlacement {
64 /// Every structure this element relates to, containment first.
65 ///
66 /// Convenience for callers that genuinely do not care which mechanism
67 /// stated the link -- but they must opt in, rather than the reader
68 /// flattening the distinction for everyone.
69 pub fn all_structures(&self) -> Vec<EntityId> {
70 let mut out: Vec<_> = self.contained_in.into_iter().collect();
71 out.extend(self.referenced_in.iter().copied());
72 out.dedup();
73 out
74 }
75}
76
77/// Read spatial containment and referencing for every element that has either.
78///
79/// Elements with no spatial relationship are absent from the map rather than
80/// present-and-empty: absence is the file's actual statement.
81pub fn spatial_placements(
82 model: &Model,
83) -> (BTreeMap<EntityId, SpatialPlacement>, Vec<SystemAnomaly>) {
84 let mut out: BTreeMap<EntityId, SpatialPlacement> = BTreeMap::new();
85 let mut anomalies = Vec::new();
86
87 // Containment: at most one per element, so a second is an anomaly.
88 for &relation in model.ids_of_type("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
89 let Some(entity) = model.get(relation) else {
90 continue;
91 };
92 let structure = match entity.attributes.get(slot::RELATING_STRUCTURE) {
93 Some(Value::Ref(id)) => *id,
94 _ => continue,
95 };
96 for element in refs(entity.attributes.get(slot::RELATED_ELEMENTS)) {
97 let slot_entry = out.entry(element).or_default();
98 match slot_entry.contained_in {
99 Some(existing) if existing != structure => {
100 // IfcElement.ContainedInStructure is SET [0:1]; two
101 // different homes cannot both be true. Keep the first by
102 // id order so the result is deterministic, and say so.
103 anomalies.push(SystemAnomaly::ContainedTwice {
104 element,
105 first: existing,
106 second: structure,
107 });
108 }
109 Some(_) => {}
110 None => slot_entry.contained_in = Some(structure),
111 }
112 }
113 }
114
115 // Referencing: many per element is legal and expected.
116 let mut references: BTreeMap<EntityId, BTreeSet<EntityId>> = BTreeMap::new();
117 for &relation in model.ids_of_type("IFCRELREFERENCEDINSPATIALSTRUCTURE") {
118 let Some(entity) = model.get(relation) else {
119 continue;
120 };
121 let structure = match entity.attributes.get(slot::RELATING_STRUCTURE) {
122 Some(Value::Ref(id)) => *id,
123 _ => continue,
124 };
125 for element in refs(entity.attributes.get(slot::RELATED_ELEMENTS)) {
126 references.entry(element).or_default().insert(structure);
127 }
128 }
129 for (element, structures) in references {
130 out.entry(element).or_default().referenced_in = structures.into_iter().collect();
131 }
132
133 (out, anomalies)
134}