ifc_lite_server/services/data_model/
classifications.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Classification association extraction.
6
7use super::types::{ClassificationAssociation, EntityJob};
8use ifc_lite_core::EntityDecoder;
9use rayon::prelude::*;
10use std::sync::Arc;
11
12/// Resolve an `IfcClassificationReference` / `IfcClassification` into
13/// `(identification, name, location, system_name)`. Walks `ReferencedSource`
14/// up to the owning `IfcClassification` (bounded to avoid cycles).
15fn resolve_classification(
16    decoder: &mut EntityDecoder,
17    id: u32,
18) -> (
19    Option<String>,
20    Option<String>,
21    Option<String>,
22    Option<String>,
23) {
24    let Ok(entity) = decoder.decode_by_id(id) else {
25        return (None, None, None, None);
26    };
27
28    if entity
29        .ifc_type
30        .as_str()
31        .eq_ignore_ascii_case("IFCCLASSIFICATION")
32    {
33        // Directly an IfcClassification: Name is attribute 3.
34        return (
35            None,
36            None,
37            None,
38            entity.get_string(3).map(|s| s.to_string()),
39        );
40    }
41
42    // IfcClassificationReference: Location(0), Identification(1), Name(2),
43    // ReferencedSource(3).
44    let location = entity.get_string(0).map(|s| s.to_string());
45    let identification = entity.get_string(1).map(|s| s.to_string());
46    let name = entity.get_string(2).map(|s| s.to_string());
47
48    // Walk ReferencedSource up to the IfcClassification for the system name.
49    let mut system_name = None;
50    let mut source = entity.get_ref(3);
51    let mut depth = 0;
52    while let Some(src_id) = source {
53        if depth >= 8 {
54            break;
55        }
56        depth += 1;
57        let Ok(src) = decoder.decode_by_id(src_id) else {
58            break;
59        };
60        if src
61            .ifc_type
62            .as_str()
63            .eq_ignore_ascii_case("IFCCLASSIFICATION")
64        {
65            system_name = src.get_string(3).map(|s| s.to_string());
66            break;
67        }
68        // Another IfcClassificationReference — keep walking its ReferencedSource.
69        source = src.get_ref(3);
70    }
71
72    (identification, name, location, system_name)
73}
74
75/// Extract classification associations (`IfcRelAssociatesClassification`).
76pub(super) fn extract_classifications(
77    jobs: &[EntityJob],
78    content: &Arc<Vec<u8>>,
79    entity_index: &Arc<ifc_lite_core::EntityIndex>,
80) -> Vec<ClassificationAssociation> {
81    let rel_jobs: Vec<_> = jobs
82        .iter()
83        .filter(|job| {
84            job.type_name
85                .eq_ignore_ascii_case("IFCRELASSOCIATESCLASSIFICATION")
86        })
87        .collect();
88
89    tracing::debug!(count = rel_jobs.len(), "Extracting classifications");
90
91    rel_jobs
92        .par_iter()
93        .flat_map(|job| {
94            let mut decoder =
95                EntityDecoder::with_arc_index(content.as_slice(), entity_index.clone());
96            let Ok(rel) = decoder.decode_at(job.start, job.end) else {
97                return Vec::new();
98            };
99            let related = super::related_object_ids(&rel);
100            // RelatingClassification is attribute 5.
101            let Some(class_id) = rel.get_ref(5) else {
102                return Vec::new();
103            };
104            let (identification, name, location, system_name) =
105                resolve_classification(&mut decoder, class_id);
106
107            related
108                .into_iter()
109                .map(|element_id| ClassificationAssociation {
110                    element_id,
111                    system_name: system_name.clone(),
112                    identification: identification.clone(),
113                    name: name.clone(),
114                    location: location.clone(),
115                })
116                .collect()
117        })
118        .collect()
119}