ifc_properties/pset/
set.rs1use 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
34const REL_RELATED_OBJECTS: usize = 4;
36const REL_RELATING_DEFINITION: usize = 5;
37const TYPE_HAS_PROPERTY_SETS: usize = 5;
39const SET_HAS_PROPERTIES: usize = 4;
41const ROOT_NAME: usize = 2;
43const ROOT_DESCRIPTION: usize = 3;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Attachment {
48 Occurrence,
50 Type,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56#[non_exhaustive]
57pub struct PropertySet {
58 pub id: EntityId,
60 pub name: Option<Arc<str>>,
62 pub description: Option<Arc<str>>,
64 pub properties: Vec<Property>,
66}
67
68impl PropertySet {
69 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
84pub fn property_set(model: &Model, id: EntityId) -> Option<PropertySet> {
95 property_set_checked(model, id).map(|(set, _)| set)
96}
97
98pub 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#[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
162pub type AttachedSets = BTreeMap<EntityId, Vec<(Attachment, PropertySet)>>;
164
165pub 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 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 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 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 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 for found in cache.anomalies.into_values() {
255 anomalies.extend(found);
256 }
257 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
268fn 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}