Skip to main content

ifc_properties/pset/
set.rs

1//! `IfcPropertySet` and how it reaches an object.
2//!
3//! # Two attachment routes, and one of them is forbidden for types
4//!
5//! ```text
6//! IfcRelDefinesByProperties  4 = RelatedObjects  5 = RelatingPropertyDefinition
7//! IfcTypeObject              5 = HasPropertySets          (a direct attribute)
8//! ```
9//!
10//! `IfcRelDefinesByProperties` carries a WHERE rule:
11//!
12//! ```text
13//! NoRelatedTypeObject : SIZEOF(QUERY(Types <* RelatedObjects |
14//!                       'IFC4.IFCTYPEOBJECT' IN TYPEOF(Types))) = 0;
15//! ```
16//!
17//! A type object may NOT be given properties by that relationship: it holds
18//! them in its own `HasPropertySets` attribute instead. A reader that only
19//! follows the relationship finds no type properties at all, and one that
20//! accepts types through it will happily read malformed files without
21//! comment. Both routes are read here, and the forbidden combination is
22//! reported.
23
24use std::collections::BTreeMap;
25use std::sync::Arc;
26
27use ifc_model::{EntityId, Model, Value};
28use ifc_schema::ifc4;
29
30use crate::error::PropertyAnomaly;
31use crate::nesting::Nesting;
32use crate::pset::scalar::{read_property, Property};
33
34/// `IfcRelDefinesByProperties`: objects at slot 4, definition at slot 5.
35const REL_RELATED_OBJECTS: usize = 4;
36const REL_RELATING_DEFINITION: usize = 5;
37/// `IfcTypeObject.HasPropertySets`.
38const TYPE_HAS_PROPERTY_SETS: usize = 5;
39/// `IfcPropertySet.HasProperties`.
40const SET_HAS_PROPERTIES: usize = 4;
41/// `IfcRoot.Name` / `Description`.
42const ROOT_NAME: usize = 2;
43const ROOT_DESCRIPTION: usize = 3;
44
45/// How a property set reached the object that carries it.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Attachment {
48    /// Attached to an occurrence by `IfcRelDefinesByProperties`.
49    Occurrence,
50    /// Held directly by an `IfcTypeObject` in `HasPropertySets`.
51    Type,
52}
53
54/// An `IfcPropertySet` with its properties resolved.
55#[derive(Debug, Clone, PartialEq)]
56pub struct PropertySet {
57    /// The `IfcPropertySet` entity.
58    pub id: EntityId,
59    /// `Name`. The schema requires it (`ExistsName`).
60    pub name: Option<Arc<str>>,
61    /// `Description`, when stated.
62    pub description: Option<Arc<str>>,
63    /// Properties in file order.
64    pub properties: Vec<Property>,
65}
66
67impl PropertySet {
68    /// Look up a property by name.
69    ///
70    /// The schema requires unique property names within a set
71    /// (`UniquePropertyNames`), so the first match is the only match in a
72    /// well-formed file. In a malformed one this is the first in
73    /// `HasProperties` order, and
74    /// [`property_sets_by_object`] reports the others as
75    /// [`PropertyAnomaly::DuplicatePropertyName`].
76    pub fn property(&self, name: &str) -> Option<&Property> {
77        self.properties
78            .iter()
79            .find(|p| p.name.as_deref() == Some(name))
80    }
81}
82
83/// Read one `IfcPropertySet` by id, resolving its properties.
84///
85/// Returns `None` when the entity is absent or is not a property set. Other
86/// `IfcPropertySetDefinition` subtypes (quantity sets, predefined sets) are
87/// deliberately excluded: they are not `IfcPropertySet` and have their own
88/// attribute layouts.
89///
90/// Members this reader cannot resolve (absent ids, cyclic or over-deep
91/// complex properties) are left out without saying so; use
92/// [`property_set_checked`] to have each one reported.
93pub fn property_set(model: &Model, id: EntityId) -> Option<PropertySet> {
94    property_set_checked(model, id).map(|(set, _)| set)
95}
96
97/// Read one `IfcPropertySet` by id, reporting every member left unread.
98///
99/// The same value as [`property_set`], with a
100/// [`PropertyAnomaly::MissingMember`] for each `HasProperties` id absent
101/// from the file, a [`PropertyAnomaly::MemberNotReference`] for each item
102/// that is not an entity reference, a [`PropertyAnomaly::DuplicateMember`]
103/// for each member listed again (read once), and the nesting anomalies
104/// described on
105/// [`property_checked`](crate::property_checked) for its complex properties.
106pub fn property_set_checked(
107    model: &Model,
108    id: EntityId,
109) -> Option<(PropertySet, Vec<PropertyAnomaly>)> {
110    let entity = model.get(id)?;
111    if !entity.type_name.eq_ignore_ascii_case("IFCPROPERTYSET") {
112        return None;
113    }
114    let mut anomalies = Vec::new();
115    let mut nesting = Nesting::new(&mut anomalies);
116    let mut properties = Vec::new();
117    let members = nesting.members(
118        id,
119        "HasProperties",
120        entity.attributes.get(SET_HAS_PROPERTIES),
121    );
122    for member in members {
123        if !nesting.admit(model, id, member) {
124            continue;
125        }
126        if let Some(property) = read_property(model, member, &mut nesting) {
127            properties.push(property);
128        }
129    }
130    let set = PropertySet {
131        id,
132        name: entity.attributes.get(ROOT_NAME).and_then(text),
133        description: entity.attributes.get(ROOT_DESCRIPTION).and_then(text),
134        properties,
135    };
136    Some((set, anomalies))
137}
138
139/// Read each property set once, however many objects share it, keeping
140/// its anomalies for one report.
141#[derive(Default)]
142struct SetCache {
143    sets: BTreeMap<EntityId, Option<PropertySet>>,
144    anomalies: BTreeMap<EntityId, Vec<PropertyAnomaly>>,
145}
146
147impl SetCache {
148    fn get(&mut self, model: &Model, id: EntityId) -> Option<PropertySet> {
149        if let Some(set) = self.sets.get(&id) {
150            return set.clone();
151        }
152        let read = property_set_checked(model, id).map(|(set, anomalies)| {
153            self.anomalies.insert(id, anomalies);
154            set
155        });
156        self.sets.insert(id, read.clone());
157        read
158    }
159}
160
161/// Property sets found on each object, with how each one was attached.
162pub type AttachedSets = BTreeMap<EntityId, Vec<(Attachment, PropertySet)>>;
163
164/// Every property set in the file, keyed by the object carrying it.
165///
166/// Both routes are followed. A set reachable by both appears once per object,
167/// with `Attachment` recording how it arrived: precedence is a caller
168/// decision, so the reader must not collapse the distinction here.
169///
170/// Anomalies report a type object attached by the forbidden relationship,
171/// relationships whose targets are missing from the file, and, once per set,
172/// the members [`property_set_checked`] could not resolve.
173pub fn property_sets_by_object(model: &Model) -> (AttachedSets, Vec<PropertyAnomaly>) {
174    let mut out: AttachedSets = BTreeMap::new();
175    let mut anomalies = Vec::new();
176    let mut cache = SetCache::default();
177    let schema = ifc4();
178
179    // Route 1: IfcRelDefinesByProperties, for occurrences.
180    for &id in model.ids_of_type("IFCRELDEFINESBYPROPERTIES") {
181        let Some(rel) = model.get(id) else { continue };
182        let Some(definition) = rel
183            .attributes
184            .get(REL_RELATING_DEFINITION)
185            .and_then(one_ref)
186        else {
187            continue;
188        };
189        let Some(set) = cache.get(model, definition) else {
190            // Quantity sets travel this relationship too and are read by the
191            // quantity module; only note a definition that is not in the file.
192            if model.get(definition).is_none() {
193                anomalies.push(PropertyAnomaly::MissingDefinition {
194                    relationship: id,
195                    definition,
196                });
197            }
198            continue;
199        };
200        for object in rel
201            .attributes
202            .get(REL_RELATED_OBJECTS)
203            .and_then(refs)
204            .unwrap_or_default()
205        {
206            let Some(target) = model.get(object) else {
207                anomalies.push(PropertyAnomaly::MissingObject {
208                    relationship: id,
209                    object,
210                });
211                continue;
212            };
213            // The WHERE rule: a type object must not appear here.
214            if schema.is_a(&target.type_name.to_ascii_uppercase(), "IFCTYPEOBJECT") {
215                anomalies.push(PropertyAnomaly::TypeAttachedByRelationship {
216                    relationship: id,
217                    type_object: object,
218                });
219            }
220            out.entry(object)
221                .or_default()
222                .push((Attachment::Occurrence, set.clone()));
223        }
224    }
225
226    // Route 2: IfcTypeObject.HasPropertySets, a direct attribute.
227    for (type_name, _) in model.type_histogram() {
228        let upper = type_name.to_ascii_uppercase();
229        if !schema.is_a(&upper, "IFCTYPEOBJECT") {
230            continue;
231        }
232        for &id in model.ids_of_type(&upper) {
233            let Some(entity) = model.get(id) else {
234                continue;
235            };
236            for set_id in entity
237                .attributes
238                .get(TYPE_HAS_PROPERTY_SETS)
239                .and_then(refs)
240                .unwrap_or_default()
241            {
242                if let Some(set) = cache.get(model, set_id) {
243                    out.entry(id).or_default().push((Attachment::Type, set));
244                }
245            }
246        }
247    }
248
249    for sets in out.values_mut() {
250        sets.sort_by_key(|(_, set)| set.id);
251    }
252    // Member anomalies, once per set and in set-id order.
253    for found in cache.anomalies.into_values() {
254        anomalies.extend(found);
255    }
256    // A set shared by many objects is checked once.
257    let mut unique: BTreeMap<EntityId, &PropertySet> = BTreeMap::new();
258    for (_, set) in out.values().flatten() {
259        unique.entry(set.id).or_insert(set);
260    }
261    for set in unique.values() {
262        duplicate_property_names(set, &mut anomalies);
263    }
264    (out, anomalies)
265}
266
267/// `UniquePropertyNames` (IFC4) / `WR32` (IFC2X3): report each property whose
268/// name an earlier one in the same set already has.
269fn duplicate_property_names(set: &PropertySet, anomalies: &mut Vec<PropertyAnomaly>) {
270    let mut first: BTreeMap<&str, EntityId> = BTreeMap::new();
271    for property in &set.properties {
272        let Some(name) = property.name.as_deref() else {
273            continue;
274        };
275        match first.get(name) {
276            Some(&kept) => anomalies.push(PropertyAnomaly::DuplicatePropertyName {
277                set: set.id,
278                kept,
279                rejected: property.id,
280            }),
281            None => {
282                first.insert(name, property.id);
283            }
284        }
285    }
286}
287
288fn text(value: &Value) -> Option<Arc<str>> {
289    match value.unwrap_typed() {
290        Value::Text(t) => Some(t.clone()),
291        _ => None,
292    }
293}
294
295fn one_ref(value: &Value) -> Option<EntityId> {
296    match value.unwrap_typed() {
297        Value::Ref(id) => Some(*id),
298        _ => None,
299    }
300}
301
302fn refs(value: &Value) -> Option<Vec<EntityId>> {
303    match value {
304        Value::List(items) => Some(items.iter().filter_map(one_ref).collect()),
305        _ => None,
306    }
307}