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
33fn require_select(
34    view: ClassificationView<'_>,
35    source: EntityId,
36    target: EntityId,
37) -> ClassificationResult<()> {
38    let entity = view
39        .model()
40        .get(target)
41        .ok_or(ClassificationError::DanglingReference {
42            entity: "IFCRELASSOCIATESCLASSIFICATION",
43            id: source,
44            attribute: "RelatingClassification",
45            target,
46        })?;
47    if entity.is_type("IFCCLASSIFICATION") || entity.is_type("IFCCLASSIFICATIONREFERENCE") {
48        Ok(())
49    } else {
50        Err(ClassificationError::ReferenceType {
51            entity: "IFCRELASSOCIATESCLASSIFICATION",
52            id: source,
53            attribute: "RelatingClassification",
54            target,
55            expected: "IfcClassificationSelect",
56            actual: entity.type_name.to_string(),
57        })
58    }
59}
60
61impl<'m> ClassificationView<'m> {
62    /// 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`.
63    pub fn hierarchy_from(
64        self,
65        leaf: EntityId,
66        budget: Budget,
67    ) -> ClassificationResult<ClassificationHierarchy<'m>> {
68        let mut current = leaf;
69        let mut from_reference = None;
70        let mut references: Vec<ClassificationReference<'m>> = Vec::new();
71        let mut positions = HashMap::new();
72        let mut visited_nodes = 0usize;
73        let mut edges_followed = 0usize;
74
75        loop {
76            if let Some(&start) = positions.get(&current) {
77                let mut path: Vec<_> = references[start..].iter().map(|r| r.id()).collect();
78                path.push(current);
79                return Err(ClassificationError::Cycle { path });
80            }
81            if visited_nodes >= budget.max_nodes {
82                return Err(ClassificationError::BudgetExceeded {
83                    max_depth: budget.max_depth,
84                    max_nodes: budget.max_nodes,
85                });
86            }
87            let entity = self.model().get(current).ok_or_else(|| {
88                from_reference.map_or(
89                    ClassificationError::UnknownEntity { id: current },
90                    |source| ClassificationError::DanglingReference {
91                        entity: "IFCCLASSIFICATIONREFERENCE",
92                        id: source,
93                        attribute: "ReferencedSource",
94                        target: current,
95                    },
96                )
97            })?;
98            visited_nodes += 1;
99
100            if entity.is_type("IFCCLASSIFICATION") && from_reference.is_some() {
101                return Ok(ClassificationHierarchy {
102                    references,
103                    system: Some(ClassificationSystem::try_new(current, entity)?),
104                });
105            }
106            if !entity.is_type("IFCCLASSIFICATIONREFERENCE") {
107                if let Some(source) = from_reference {
108                    return Err(ClassificationError::ReferenceType {
109                        entity: "IFCCLASSIFICATIONREFERENCE",
110                        id: source,
111                        attribute: "ReferencedSource",
112                        target: current,
113                        expected: "IfcClassificationReferenceSelect",
114                        actual: entity.type_name.to_string(),
115                    });
116                }
117                return Err(ClassificationError::WrongEntityType {
118                    expected: "IFCCLASSIFICATIONREFERENCE",
119                    actual: entity.type_name.to_string(),
120                });
121            }
122
123            positions.insert(current, references.len());
124            let reference = ClassificationReference::try_new(current, entity)?;
125            let source = reference.referenced_source_id()?;
126            references.push(reference);
127            let Some(source) = source else {
128                return Ok(ClassificationHierarchy {
129                    references,
130                    system: None,
131                });
132            };
133            if edges_followed >= budget.max_depth {
134                return Err(ClassificationError::BudgetExceeded {
135                    max_depth: budget.max_depth,
136                    max_nodes: budget.max_nodes,
137                });
138            }
139            edges_followed += 1;
140            from_reference = Some(current);
141            current = source;
142        }
143    }
144
145    /// `IfcClassificationReference` instances whose `ReferencedSource` is `source`, sorted by id; fails if `source` is unknown or is not itself a classification or reference.
146    pub fn children_of(
147        self,
148        source: EntityId,
149    ) -> ClassificationResult<Vec<ClassificationReference<'m>>> {
150        let source_entity = self
151            .model()
152            .get(source)
153            .ok_or(ClassificationError::UnknownEntity { id: source })?;
154        if !(source_entity.is_type("IFCCLASSIFICATION")
155            || source_entity.is_type("IFCCLASSIFICATIONREFERENCE"))
156        {
157            return Err(ClassificationError::ReferenceType {
158                entity: "IFCCLASSIFICATIONREFERENCE",
159                id: source,
160                attribute: "ReferencedSource",
161                target: source,
162                expected: "IfcClassificationReferenceSelect",
163                actual: source_entity.type_name.to_string(),
164            });
165        }
166        let mut out = Vec::new();
167        for reference in self.references() {
168            if reference.referenced_source_id()? == Some(source) {
169                out.push(reference);
170            }
171        }
172        out.sort_by_key(|reference| reference.id());
173        Ok(out)
174    }
175
176    /// 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.
177    pub fn effective_classifications(
178        self,
179        object: EntityId,
180    ) -> ClassificationResult<EffectiveClassifications<'m>> {
181        if self.model().get(object).is_none() {
182            return Err(ClassificationError::UnknownEntity { id: object });
183        }
184        let occurrence = self.classification_assignments_for(object)?;
185        for assignment in &occurrence {
186            require_select(
187                self,
188                assignment.id(),
189                assignment.relating_classification_id()?,
190            )?;
191        }
192
193        let mut types = Vec::new();
194        for (relationship_id, entity) in self.model().of_type("IFCRELDEFINESBYTYPE") {
195            if required_refs(
196                "IFCRELDEFINESBYTYPE",
197                relationship_id,
198                entity,
199                4,
200                "RelatedObjects",
201            )?
202            .contains(&object)
203            {
204                let type_id = required_ref(
205                    "IFCRELDEFINESBYTYPE",
206                    relationship_id,
207                    entity,
208                    5,
209                    "RelatingType",
210                )?;
211                self.model()
212                    .get(type_id)
213                    .ok_or(ClassificationError::DanglingReference {
214                        entity: "IFCRELDEFINESBYTYPE",
215                        id: relationship_id,
216                        attribute: "RelatingType",
217                        target: type_id,
218                    })?;
219                types.push(type_id);
220            }
221        }
222        if types.len() > 1 {
223            return Err(ClassificationError::AmbiguousType {
224                object,
225                count: types.len(),
226            });
227        }
228        let type_id = types.first().copied();
229        let inherited_from_type = if let Some(type_id) = type_id {
230            let assignments = self.classification_assignments_for(type_id)?;
231            for assignment in &assignments {
232                require_select(
233                    self,
234                    assignment.id(),
235                    assignment.relating_classification_id()?,
236                )?;
237            }
238            assignments
239        } else {
240            Vec::new()
241        };
242        Ok(EffectiveClassifications {
243            occurrence,
244            type_object: type_id,
245            inherited: inherited_from_type,
246        })
247    }
248}