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