Skip to main content

ifc_classification/query/
hierarchy.rs

1//! Bounded classification hierarchy and explicit occurrence/type lookup.
2
3use std::collections::HashMap;
4
5use ifc_model::{Budget, EntityId};
6
7use crate::view::{required_ref, required_refs};
8use crate::{
9    ClassificationAssignment, ClassificationError, ClassificationReference, ClassificationResult,
10    ClassificationSystem, ClassificationView,
11};
12
13/// Chain of `IfcClassificationReference` nodes from a leaf up to its root, plus the terminating `IfcClassification` system if the chain resolved fully.
14#[derive(Debug, Clone)]
15pub struct ClassificationHierarchy<'m> {
16    /// References from the queried leaf (index 0) up through its ancestors, in traversal order.
17    pub references: Vec<ClassificationReference<'m>>,
18    /// The root `IfcClassification` system, when the chain terminated there rather than at an unlinked reference.
19    pub system: Option<ClassificationSystem<'m>>,
20}
21
22/// Classification assignments effective on an object: those directly assigned plus, when the object has an `IfcRelDefinesByType` type, those inherited from that type.
23#[derive(Debug, Clone)]
24pub struct EffectiveClassifications<'m> {
25    /// Assignments made directly on the queried occurrence object.
26    pub occurrence: Vec<ClassificationAssignment<'m>>,
27    /// Id of the object's `IfcRelDefinesByType` type, when unambiguous.
28    pub type_object: Option<EntityId>,
29    /// Assignments made on the type object, inherited by the occurrence.
30    pub inherited: Vec<ClassificationAssignment<'m>>,
31}
32
33/// Check `target` against the bound release's `RelatingClassification`
34/// type: `IfcClassificationSelect` in IFC4, `IfcClassificationNotationSelect`
35/// (notation or reference) in IFC2X3.
36fn require_select(
37    view: ClassificationView<'_>,
38    source: EntityId,
39    target: EntityId,
40) -> ClassificationResult<()> {
41    const RELATION: &str = "IFCRELASSOCIATESCLASSIFICATION";
42    let entity = view
43        .model()
44        .get(target)
45        .ok_or(ClassificationError::DanglingReference {
46            entity: RELATION,
47            id: source,
48            attribute: "RelatingClassification",
49            target,
50        })?;
51    let release = view.release();
52    if release.accepts(
53        RELATION,
54        source,
55        "RelatingClassification",
56        &entity.type_name,
57    )? {
58        Ok(())
59    } else {
60        Err(ClassificationError::ReferenceType {
61            entity: RELATION,
62            id: source,
63            attribute: "RelatingClassification",
64            target,
65            expected: release.declared_type(RELATION, source, "RelatingClassification")?,
66            actual: entity.type_name.to_string(),
67        })
68    }
69}
70
71impl<'m> ClassificationView<'m> {
72    /// Walk `ReferencedSource` links from `leaf` up to its root, stopping at the enclosing `IfcClassification` if present; fails on a cycle, a dangling or mistyped source, or if the walk exceeds `budget`.
73    pub fn hierarchy_from(
74        self,
75        leaf: EntityId,
76        budget: Budget,
77    ) -> ClassificationResult<ClassificationHierarchy<'m>> {
78        let mut current = leaf;
79        let mut from_reference = None;
80        let mut references: Vec<ClassificationReference<'m>> = Vec::new();
81        let mut positions = HashMap::new();
82        let mut visited_nodes = 0usize;
83        let mut edges_followed = 0usize;
84
85        loop {
86            if let Some(&start) = positions.get(&current) {
87                let mut path: Vec<_> = references[start..].iter().map(|r| r.id()).collect();
88                path.push(current);
89                return Err(ClassificationError::Cycle { path });
90            }
91            if visited_nodes >= budget.max_nodes {
92                return Err(ClassificationError::BudgetExceeded {
93                    max_depth: budget.max_depth,
94                    max_nodes: budget.max_nodes,
95                });
96            }
97            let entity = self.model().get(current).ok_or_else(|| {
98                from_reference.map_or(
99                    ClassificationError::UnknownEntity { id: current },
100                    |source| ClassificationError::DanglingReference {
101                        entity: "IFCCLASSIFICATIONREFERENCE",
102                        id: source,
103                        attribute: "ReferencedSource",
104                        target: current,
105                    },
106                )
107            })?;
108            visited_nodes += 1;
109
110            // A followed `ReferencedSource` must satisfy the bound release's
111            // declared type: `IfcClassificationReferenceSelect` in IFC4,
112            // `IfcClassification` only in IFC2X3 (no reference-to-reference
113            // chains there).
114            if let Some(source) = from_reference {
115                let release = self.release();
116                if !release.accepts(
117                    "IFCCLASSIFICATIONREFERENCE",
118                    source,
119                    "ReferencedSource",
120                    &entity.type_name,
121                )? {
122                    return Err(ClassificationError::ReferenceType {
123                        entity: "IFCCLASSIFICATIONREFERENCE",
124                        id: source,
125                        attribute: "ReferencedSource",
126                        target: current,
127                        expected: release.declared_type(
128                            "IFCCLASSIFICATIONREFERENCE",
129                            source,
130                            "ReferencedSource",
131                        )?,
132                        actual: entity.type_name.to_string(),
133                    });
134                }
135            }
136            if entity.is_type("IFCCLASSIFICATION") && from_reference.is_some() {
137                return Ok(ClassificationHierarchy {
138                    references,
139                    system: Some(ClassificationSystem::try_bound(
140                        current,
141                        entity,
142                        self.release(),
143                    )?),
144                });
145            }
146            if !entity.is_type("IFCCLASSIFICATIONREFERENCE") {
147                if let Some(source) = from_reference {
148                    return Err(ClassificationError::ReferenceType {
149                        entity: "IFCCLASSIFICATIONREFERENCE",
150                        id: source,
151                        attribute: "ReferencedSource",
152                        target: current,
153                        expected: "IfcClassificationReferenceSelect",
154                        actual: entity.type_name.to_string(),
155                    });
156                }
157                return Err(ClassificationError::WrongEntityType {
158                    expected: "IFCCLASSIFICATIONREFERENCE",
159                    actual: entity.type_name.to_string(),
160                });
161            }
162
163            positions.insert(current, references.len());
164            let reference = ClassificationReference::try_bound(current, entity, self.release())?;
165            let source = reference.referenced_source_id()?;
166            references.push(reference);
167            let Some(source) = source else {
168                return Ok(ClassificationHierarchy {
169                    references,
170                    system: None,
171                });
172            };
173            if edges_followed >= budget.max_depth {
174                return Err(ClassificationError::BudgetExceeded {
175                    max_depth: budget.max_depth,
176                    max_nodes: budget.max_nodes,
177                });
178            }
179            edges_followed += 1;
180            from_reference = Some(current);
181            current = source;
182        }
183    }
184
185    /// `IfcClassificationReference` instances whose `ReferencedSource` is `source`, sorted by id; fails if `source` is unknown or is not itself a classification or reference.
186    pub fn children_of(
187        self,
188        source: EntityId,
189    ) -> ClassificationResult<Vec<ClassificationReference<'m>>> {
190        let source_entity = self
191            .model()
192            .get(source)
193            .ok_or(ClassificationError::UnknownEntity { id: source })?;
194        let release = self.release();
195        if !release.accepts(
196            "IFCCLASSIFICATIONREFERENCE",
197            source,
198            "ReferencedSource",
199            &source_entity.type_name,
200        )? {
201            return Err(ClassificationError::ReferenceType {
202                entity: "IFCCLASSIFICATIONREFERENCE",
203                id: source,
204                attribute: "ReferencedSource",
205                target: source,
206                expected: release.declared_type(
207                    "IFCCLASSIFICATIONREFERENCE",
208                    source,
209                    "ReferencedSource",
210                )?,
211                actual: source_entity.type_name.to_string(),
212            });
213        }
214        let mut out = Vec::new();
215        for reference in self.references() {
216            if reference.referenced_source_id()? == Some(source) {
217                out.push(reference);
218            }
219        }
220        out.sort_by_key(|reference| reference.id());
221        Ok(out)
222    }
223
224    /// Direct classification assignments on `object`, plus any inherited from its unambiguous `IfcRelDefinesByType` type; fails if `object` is unknown, has an assignment with an invalid relating classification, or is related to more than one type.
225    pub fn effective_classifications(
226        self,
227        object: EntityId,
228    ) -> ClassificationResult<EffectiveClassifications<'m>> {
229        if self.model().get(object).is_none() {
230            return Err(ClassificationError::UnknownEntity { id: object });
231        }
232        let occurrence = self.classification_assignments_for(object)?;
233        for assignment in &occurrence {
234            require_select(
235                self,
236                assignment.id(),
237                assignment.relating_classification_id()?,
238            )?;
239        }
240
241        let mut types = Vec::new();
242        for (relationship_id, entity) in self.model().of_type("IFCRELDEFINESBYTYPE") {
243            if required_refs(
244                "IFCRELDEFINESBYTYPE",
245                relationship_id,
246                entity,
247                4,
248                "RelatedObjects",
249            )?
250            .contains(&object)
251            {
252                let type_id = required_ref(
253                    "IFCRELDEFINESBYTYPE",
254                    relationship_id,
255                    entity,
256                    5,
257                    "RelatingType",
258                )?;
259                self.model()
260                    .get(type_id)
261                    .ok_or(ClassificationError::DanglingReference {
262                        entity: "IFCRELDEFINESBYTYPE",
263                        id: relationship_id,
264                        attribute: "RelatingType",
265                        target: type_id,
266                    })?;
267                types.push(type_id);
268            }
269        }
270        if types.len() > 1 {
271            return Err(ClassificationError::AmbiguousType {
272                object,
273                count: types.len(),
274            });
275        }
276        let type_id = types.first().copied();
277        let inherited_from_type = if let Some(type_id) = type_id {
278            let assignments = self.classification_assignments_for(type_id)?;
279            for assignment in &assignments {
280                require_select(
281                    self,
282                    assignment.id(),
283                    assignment.relating_classification_id()?,
284                )?;
285            }
286            assignments
287        } else {
288            Vec::new()
289        };
290        Ok(EffectiveClassifications {
291            occurrence,
292            type_object: type_id,
293            inherited: inherited_from_type,
294        })
295    }
296}