use std::collections::BTreeMap;
use ifc_model::{EntityId, Model, ReverseIndex, Value};
use ifc_properties::{exact_properties, ExactSource, SchemaVersion};
use ifc_schema::Schema;
use super::edit::PropertyEditFailure;
#[derive(Debug, Clone, Copy)]
pub(super) struct Release {
pub(super) version: SchemaVersion,
pub(super) schema: &'static Schema,
}
impl Release {
pub(super) fn slot(&self, entity: &str, attribute: &str) -> Option<usize> {
self.schema
.attribute_names(entity)
.iter()
.position(|name| name.eq_ignore_ascii_case(attribute))
}
pub(super) fn attribute<'m>(
&self,
model: &'m Model,
id: EntityId,
attribute: &str,
) -> Option<&'m Value> {
let entity = model.get(id)?;
let slot = self.slot(&entity.type_name, attribute)?;
entity.attributes.get(slot)
}
pub(super) fn text<'m>(
&self,
model: &'m Model,
id: EntityId,
attribute: &str,
) -> Option<&'m str> {
match self.attribute(model, id, attribute)?.unwrap_typed() {
Value::Text(text) => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub(super) struct OwnSet {
pub(super) id: EntityId,
pub(super) type_name: String,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Inherited {
pub(super) type_object: EntityId,
pub(super) property: EntityId,
}
#[derive(Debug, Clone)]
pub(super) struct Holder {
pub(super) id: EntityId,
pub(super) is_type: bool,
pub(super) global_id: Option<String>,
pub(super) owner_history: Option<EntityId>,
pub(super) own: BTreeMap<String, OwnSet>,
pub(super) inherited_sets: BTreeMap<String, (EntityId, String)>,
pub(super) inherited: BTreeMap<(String, String), Inherited>,
}
impl Holder {
pub(super) fn read(
model: &Model,
release: Release,
id: EntityId,
) -> Result<Self, PropertyEditFailure> {
let entity = model
.get(id)
.ok_or(PropertyEditFailure::MissingEntity(id))?;
let entries = exact_properties(model, id).map_err(PropertyEditFailure::Resolve)?;
let is_type = release.schema.is_a(&entity.type_name, "IFCTYPEOBJECT");
let mut holder = Self {
id,
is_type,
global_id: release.text(model, id, "GlobalId").map(str::to_owned),
owner_history: release
.attribute(model, id, "OwnerHistory")
.and_then(Value::as_ref_id),
own: BTreeMap::new(),
inherited_sets: BTreeMap::new(),
inherited: BTreeMap::new(),
};
for entry in &entries {
let property = &entry.property;
let set_name = property.property_set.to_string();
let set_type = model
.get(property.set_id)
.map(|set| set.type_name.to_ascii_uppercase())
.unwrap_or_default();
let own = match property.source {
ExactSource::Occurrence => !is_type,
ExactSource::Type(type_object) => {
if is_type {
type_object == id
} else {
holder
.inherited_sets
.entry(set_name.clone())
.or_insert((type_object, set_type.clone()));
holder.inherited.insert(
(set_name.clone(), entry.name.to_string()),
Inherited {
type_object,
property: property.property_id,
},
);
false
}
}
_ => false,
};
if !own {
continue;
}
match holder.own.get(&set_name) {
Some(existing) if existing.id != property.set_id => {
return Err(PropertyEditFailure::InvalidModel(format!(
"{id} states two sets named {set_name}: {} and {}",
existing.id, property.set_id
)));
}
Some(_) => {}
None => {
holder.own.insert(
set_name,
OwnSet {
id: property.set_id,
type_name: set_type,
},
);
}
}
}
Ok(holder)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Link {
Rel(EntityId),
Type,
}
pub(super) fn holding(
model: &Model,
release: Release,
reverse: &ReverseIndex,
holder: &Holder,
set: EntityId,
) -> Result<(Link, bool), PropertyEditFailure> {
let schema = release.schema;
let holders: Vec<EntityId> = reverse
.referrers(set)
.iter()
.map(|referrer| referrer.from)
.filter(|from| {
model.get(*from).is_some_and(|entity| {
schema.is_a(&entity.type_name, "IFCRELDEFINESBYPROPERTIES")
|| schema.is_a(&entity.type_name, "IFCTYPEOBJECT")
})
})
.collect();
if holder.is_type {
return Ok((Link::Type, holders == [holder.id]));
}
let related = |rel: EntityId| -> Vec<EntityId> {
release
.attribute(model, rel, "RelatedObjects")
.and_then(Value::as_list)
.map(|items| items.iter().filter_map(Value::as_ref_id).collect())
.unwrap_or_default()
};
let rels: Vec<EntityId> = holders
.iter()
.copied()
.filter(|rel| related(*rel).contains(&holder.id))
.collect();
let [rel] = rels.as_slice() else {
return Err(PropertyEditFailure::InvalidModel(format!(
"{} is related to set {set} by {} relationships, not one",
holder.id,
rels.len()
)));
};
match release.attribute(model, *rel, "RelatingPropertyDefinition") {
Some(Value::Ref(definition)) if *definition == set => {}
_ => {
return Err(PropertyEditFailure::Unsupported(format!(
"{rel} relates set {set} through an IfcPropertySetDefinitionSet, which this writer does not split"
)))
}
}
let exclusive = holders == [*rel] && related(*rel) == [holder.id];
Ok((Link::Rel(*rel), exclusive))
}
pub(super) fn sole_member(
model: &Model,
reverse: &ReverseIndex,
set: EntityId,
property: EntityId,
) -> bool {
let only_set = reverse
.referrers(property)
.iter()
.all(|referrer| referrer.from == set);
let once = model
.get(set)
.map(|entity| {
entity
.attributes
.iter()
.filter_map(Value::as_list)
.flatten()
.filter(|item| item.as_ref_id() == Some(property))
.count()
== 1
})
.unwrap_or(false);
only_set && once
}