Skip to main content

bimifc_parser/ifcx/
model.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//! IFC5 (IFCX) model implementation
6//!
7//! Implements IfcModel trait for IFCX JSON format.
8
9use super::composition::compose_nodes;
10use super::types::{attr, ComposedNode, IfcClass, IfcxFile};
11use crate::Result;
12use bimifc_model::{
13    AttributeValue, DecodedEntity, EntityId, EntityResolver, IfcModel, IfcType, ModelMetadata,
14    Property, PropertyReader, PropertySet, Quantity, SpatialNode, SpatialNodeType, SpatialQuery,
15    StoreyInfo,
16};
17use rustc_hash::FxHashMap;
18use std::sync::Arc;
19
20/// IFC5 (IFCX) model
21pub struct IfcxModel {
22    /// Composed nodes by path
23    nodes: FxHashMap<String, ComposedNode>,
24    /// Path to synthetic ID mapping
25    path_to_id: FxHashMap<String, EntityId>,
26    /// ID to path mapping
27    id_to_path: FxHashMap<EntityId, String>,
28    /// Type index for fast type lookups
29    type_index: FxHashMap<IfcType, Vec<EntityId>>,
30    /// Decoded entities cache
31    entities: FxHashMap<EntityId, Arc<DecodedEntity>>,
32    /// Spatial tree
33    spatial_tree: Option<SpatialNode>,
34    /// Model metadata
35    metadata: ModelMetadata,
36    /// Unit scale (default 1.0 for meters)
37    unit_scale: f64,
38}
39
40impl IfcxModel {
41    /// Parse IFCX JSON content
42    pub fn parse(content: &str) -> Result<Self> {
43        // Parse JSON
44        let file: IfcxFile = serde_json::from_str(content)
45            .map_err(|e| bimifc_model::ParseError::InvalidFormat(e.to_string()))?;
46
47        // Compose nodes (flatten ECS)
48        let nodes = compose_nodes(&file.data);
49
50        // Build path<->ID mappings
51        let mut path_to_id = FxHashMap::default();
52        let mut id_to_path = FxHashMap::default();
53        let mut next_id = 1u32;
54
55        for path in nodes.keys() {
56            let id = EntityId(next_id);
57            path_to_id.insert(path.clone(), id);
58            id_to_path.insert(id, path.clone());
59            next_id += 1;
60        }
61
62        // Build type index and entities
63        let mut type_index: FxHashMap<IfcType, Vec<EntityId>> = FxHashMap::default();
64        let mut entities = FxHashMap::default();
65
66        for (path, node) in &nodes {
67            let id = path_to_id[path];
68            let ifc_type = extract_ifc_type(node);
69
70            // Build decoded entity
71            let entity = Arc::new(DecodedEntity {
72                id,
73                ifc_type: ifc_type.clone(),
74                attributes: build_attributes(node, &path_to_id),
75            });
76
77            entities.insert(id, entity);
78            type_index.entry(ifc_type).or_default().push(id);
79        }
80
81        // Build spatial tree
82        let spatial_tree = build_spatial_tree(&nodes, &path_to_id);
83
84        // Extract metadata
85        let metadata = ModelMetadata {
86            schema_version: format!("IFC5 ({})", file.header.ifcx_version),
87            originating_system: Some(file.header.author.clone()),
88            file_name: Some(file.header.id.clone()),
89            timestamp: Some(file.header.timestamp.clone()),
90            ..Default::default()
91        };
92
93        Ok(Self {
94            nodes,
95            path_to_id,
96            id_to_path,
97            type_index,
98            entities,
99            spatial_tree,
100            metadata,
101            unit_scale: 1.0, // IFCX uses meters by default
102        })
103    }
104
105    /// Get node by entity ID
106    pub fn node(&self, id: EntityId) -> Option<&ComposedNode> {
107        let path = self.id_to_path.get(&id)?;
108        self.nodes.get(path)
109    }
110
111    /// Get path for entity ID
112    pub fn path(&self, id: EntityId) -> Option<&str> {
113        self.id_to_path.get(&id).map(|s| s.as_str())
114    }
115
116    /// Get entity ID for path
117    pub fn id_for_path(&self, path: &str) -> Option<EntityId> {
118        self.path_to_id.get(path).copied()
119    }
120}
121
122/// Extract IFC type from composed node
123fn extract_ifc_type(node: &ComposedNode) -> IfcType {
124    if let Some(class_val) = node.attributes.get(attr::CLASS) {
125        if let Some(class) = IfcClass::from_value(class_val) {
126            return IfcType::parse(&class.code);
127        }
128    }
129    IfcType::Unknown(String::new())
130}
131
132/// Build attribute values from composed node
133fn build_attributes(
134    node: &ComposedNode,
135    path_to_id: &FxHashMap<String, EntityId>,
136) -> Vec<AttributeValue> {
137    // For IFCX, we store key attributes in a standardized order:
138    // [0] = GlobalId (path/UUID)
139    // [1] = OwnerHistory (null for IFCX)
140    // [2] = Name
141    // [3] = Description
142    // [4] = ObjectType
143    // [5] = Children refs
144
145    let mut attrs = vec![AttributeValue::Null; 10];
146
147    // GlobalId = path
148    attrs[0] = AttributeValue::String(node.path.clone());
149
150    // OwnerHistory = null
151    attrs[1] = AttributeValue::Null;
152
153    // Name - check various property patterns
154    if let Some(name) = node
155        .attributes
156        .get("bsi::ifc::prop::Name")
157        .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
158    {
159        if let Some(s) = name.as_str() {
160            attrs[2] = AttributeValue::String(s.to_string());
161        }
162    }
163
164    // Description
165    if let Some(desc) = node.attributes.get("bsi::ifc::prop::Description") {
166        if let Some(s) = desc.as_str() {
167            attrs[3] = AttributeValue::String(s.to_string());
168        }
169    }
170
171    // Children as entity refs
172    let child_refs: Vec<AttributeValue> = node
173        .children
174        .iter()
175        .filter_map(|child_path| {
176            path_to_id
177                .get(child_path)
178                .map(|id| AttributeValue::EntityRef(*id))
179        })
180        .collect();
181
182    if !child_refs.is_empty() {
183        attrs[5] = AttributeValue::List(child_refs);
184    }
185
186    attrs
187}
188
189/// Build spatial tree from composed nodes
190fn build_spatial_tree(
191    nodes: &FxHashMap<String, ComposedNode>,
192    path_to_id: &FxHashMap<String, EntityId>,
193) -> Option<SpatialNode> {
194    // Find root (usually IfcProject)
195    let mut root_path: Option<&str> = None;
196
197    for (path, node) in nodes {
198        let ifc_type = extract_ifc_type(node);
199        if matches!(ifc_type, IfcType::IfcProject) {
200            root_path = Some(path);
201            break;
202        }
203    }
204
205    // If no project, find node with no parent
206    if root_path.is_none() {
207        for (path, node) in nodes {
208            if node.parent.is_none() && !node.children.is_empty() {
209                root_path = Some(path);
210                break;
211            }
212        }
213    }
214
215    let root_path = root_path?;
216
217    // Build tree recursively
218    fn build_node(
219        path: &str,
220        nodes: &FxHashMap<String, ComposedNode>,
221        path_to_id: &FxHashMap<String, EntityId>,
222    ) -> Option<SpatialNode> {
223        let node = nodes.get(path)?;
224        let id = *path_to_id.get(path)?;
225        let ifc_type = extract_ifc_type(node);
226
227        // Get name
228        let name = node
229            .attributes
230            .get("bsi::ifc::prop::Name")
231            .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
232            .and_then(|v| v.as_str())
233            .map(String::from)
234            .unwrap_or_else(|| path.to_string());
235
236        let node_type = SpatialNodeType::from_ifc_type(&ifc_type);
237        let entity_type = ifc_type.name().to_string();
238
239        // Check if has geometry
240        let has_geometry = node.attributes.contains_key(attr::MESH);
241
242        // Build children recursively
243        let children: Vec<SpatialNode> = node
244            .children
245            .iter()
246            .filter_map(|child_path| build_node(child_path, nodes, path_to_id))
247            .collect();
248
249        let mut spatial_node = SpatialNode::new(id, node_type, name, entity_type);
250        spatial_node.children = children;
251        spatial_node.has_geometry = has_geometry;
252
253        Some(spatial_node)
254    }
255
256    build_node(root_path, nodes, path_to_id)
257}
258
259// Implement IfcModel trait
260impl IfcModel for IfcxModel {
261    fn resolver(&self) -> &dyn EntityResolver {
262        self
263    }
264
265    fn properties(&self) -> &dyn PropertyReader {
266        self
267    }
268
269    fn spatial(&self) -> &dyn SpatialQuery {
270        self
271    }
272
273    fn unit_scale(&self) -> f64 {
274        self.unit_scale
275    }
276
277    fn metadata(&self) -> &ModelMetadata {
278        &self.metadata
279    }
280}
281
282// Implement EntityResolver
283impl EntityResolver for IfcxModel {
284    fn get(&self, id: EntityId) -> Option<Arc<DecodedEntity>> {
285        self.entities.get(&id).cloned()
286    }
287
288    fn entities_by_type(&self, ifc_type: &IfcType) -> Vec<Arc<DecodedEntity>> {
289        self.type_index
290            .get(ifc_type)
291            .map(|ids| {
292                ids.iter()
293                    .filter_map(|id| self.entities.get(id).cloned())
294                    .collect()
295            })
296            .unwrap_or_default()
297    }
298
299    fn find_by_type_name(&self, type_name: &str) -> Vec<Arc<DecodedEntity>> {
300        let target = IfcType::parse(type_name);
301        self.entities_by_type(&target)
302    }
303
304    fn count_by_type(&self, ifc_type: &IfcType) -> usize {
305        self.type_index.get(ifc_type).map(|v| v.len()).unwrap_or(0)
306    }
307
308    fn all_ids(&self) -> Vec<EntityId> {
309        self.entities.keys().copied().collect()
310    }
311
312    fn raw_bytes(&self, _id: EntityId) -> Option<&[u8]> {
313        // Not applicable for JSON format
314        None
315    }
316}
317
318// Implement PropertyReader
319impl PropertyReader for IfcxModel {
320    fn property_sets(&self, id: EntityId) -> Vec<PropertySet> {
321        let Some(node) = self.node(id) else {
322            return Vec::new();
323        };
324
325        // Group attributes by namespace prefix as "property sets"
326        let mut psets: FxHashMap<String, Vec<Property>> = FxHashMap::default();
327
328        for (key, value) in &node.attributes {
329            // Skip non-property attributes
330            if key.starts_with("usd::") || key == attr::CLASS || key == attr::MATERIAL {
331                continue;
332            }
333
334            // Extract namespace and property name
335            let (namespace, prop_name) = if let Some(pos) = key.rfind("::") {
336                (key[..pos].to_string(), key[pos + 2..].to_string())
337            } else {
338                ("Properties".to_string(), key.clone())
339            };
340
341            // Convert JSON value to string
342            let prop_value = json_to_string(value);
343
344            psets
345                .entry(namespace)
346                .or_default()
347                .push(Property::new(prop_name, prop_value));
348        }
349
350        psets
351            .into_iter()
352            .map(|(name, properties)| PropertySet { name, properties })
353            .collect()
354    }
355
356    fn quantities(&self, _id: EntityId) -> Vec<Quantity> {
357        // Quantities in IFCX are just namespaced properties
358        // Could filter for "bsi::ifc::qto::" prefix
359        Vec::new()
360    }
361
362    fn global_id(&self, id: EntityId) -> Option<String> {
363        // GlobalId is the path (UUID)
364        self.path(id).map(String::from)
365    }
366
367    fn name(&self, id: EntityId) -> Option<String> {
368        let node = self.node(id)?;
369        node.attributes
370            .get("bsi::ifc::prop::Name")
371            .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
372            .and_then(|v| v.as_str())
373            .map(String::from)
374    }
375
376    fn description(&self, id: EntityId) -> Option<String> {
377        let node = self.node(id)?;
378        node.attributes
379            .get("bsi::ifc::prop::Description")
380            .and_then(|v| v.as_str())
381            .map(String::from)
382    }
383}
384
385/// Convert JSON value to string for property display
386fn json_to_string(value: &serde_json::Value) -> String {
387    match value {
388        serde_json::Value::Bool(b) => b.to_string(),
389        serde_json::Value::Number(n) => n.to_string(),
390        serde_json::Value::String(s) => s.clone(),
391        serde_json::Value::Array(arr) => {
392            let items: Vec<String> = arr.iter().map(json_to_string).collect();
393            format!("[{}]", items.join(", "))
394        }
395        serde_json::Value::Object(obj) => {
396            // For objects, try to extract meaningful value
397            if let Some(code) = obj.get("code").and_then(|v| v.as_str()) {
398                code.to_string()
399            } else {
400                value.to_string()
401            }
402        }
403        serde_json::Value::Null => "null".to_string(),
404    }
405}
406
407// Implement SpatialQuery
408impl SpatialQuery for IfcxModel {
409    fn spatial_tree(&self) -> Option<&SpatialNode> {
410        self.spatial_tree.as_ref()
411    }
412
413    fn storeys(&self) -> Vec<StoreyInfo> {
414        let Some(tree) = &self.spatial_tree else {
415            return Vec::new();
416        };
417
418        let mut storeys = Vec::new();
419
420        fn find_storeys(node: &SpatialNode, storeys: &mut Vec<StoreyInfo>) {
421            if node.node_type == SpatialNodeType::Storey {
422                storeys.push(StoreyInfo {
423                    id: node.id,
424                    name: node.name.clone(),
425                    elevation: node.elevation.unwrap_or(0.0),
426                    element_count: node.element_count(),
427                });
428            }
429            for child in &node.children {
430                find_storeys(child, storeys);
431            }
432        }
433
434        find_storeys(tree, &mut storeys);
435        storeys.sort_by(|a, b| a.elevation.partial_cmp(&b.elevation).unwrap());
436        storeys
437    }
438
439    fn elements_in_storey(&self, storey_id: EntityId) -> Vec<EntityId> {
440        let Some(tree) = &self.spatial_tree else {
441            return Vec::new();
442        };
443
444        if let Some(storey_node) = tree.find(storey_id) {
445            storey_node.element_ids()
446        } else {
447            Vec::new()
448        }
449    }
450
451    fn containing_storey(&self, element_id: EntityId) -> Option<EntityId> {
452        // Walk up the parent chain from the element
453        let path = self.id_to_path.get(&element_id)?;
454        let mut current_path = self.nodes.get(path)?.parent.clone();
455
456        while let Some(p) = current_path {
457            let node = self.nodes.get(&p)?;
458            let ifc_type = extract_ifc_type(node);
459            if matches!(ifc_type, IfcType::IfcBuildingStorey) {
460                return self.path_to_id.get(&p).copied();
461            }
462            current_path = node.parent.clone();
463        }
464
465        None
466    }
467
468    fn search(&self, query: &str) -> Vec<EntityId> {
469        let query_lower = query.to_lowercase();
470        let mut results = Vec::new();
471
472        for (path, node) in &self.nodes {
473            // Search in name
474            if let Some(name) = node
475                .attributes
476                .get("bsi::ifc::prop::Name")
477                .or_else(|| node.attributes.get("bsi::ifc::prop::TypeName"))
478                .and_then(|v| v.as_str())
479            {
480                if name.to_lowercase().contains(&query_lower) {
481                    if let Some(id) = self.path_to_id.get(path) {
482                        results.push(*id);
483                        continue;
484                    }
485                }
486            }
487
488            // Search in type
489            let ifc_type = extract_ifc_type(node);
490            if ifc_type.name().to_lowercase().contains(&query_lower) {
491                if let Some(id) = self.path_to_id.get(path) {
492                    results.push(*id);
493                }
494            }
495        }
496
497        results
498    }
499
500    fn elements_by_type(&self, ifc_type: &IfcType) -> Vec<EntityId> {
501        self.type_index.get(ifc_type).cloned().unwrap_or_default()
502    }
503}