use std::{collections::BTreeMap, sync::Arc};
use ifc_model::{Entity, EntityId, Model, Value};
use super::complex::{complex_value, is_complex};
use super::composite::composite_value;
use super::predefined::{attribute_value, own_attributes, predefined_key};
use super::quantity::{quantity_members, quantity_value, slot};
use super::refs::{nonempty_refs_at, text_at};
use super::release::Release;
use super::value::{exact_property_value, ResolvedValue};
use super::{ExactProperty, ExactPropertyError, ExactSource};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SetKind {
Properties,
Quantities,
Predefined,
MaterialProperties,
}
#[derive(Debug, Clone, Copy)]
pub(super) enum Member {
Entity(EntityId),
Attribute(usize),
}
pub(super) struct LoadedSet<'m> {
pub(super) id: EntityId,
pub(super) entity: &'m Entity,
pub(super) kind: SetKind,
pub(super) name: &'m str,
pub(super) named: bool,
}
pub(super) fn load_set(
model: &Model,
release: Release,
set_id: EntityId,
) -> Result<(&Entity, SetKind), ExactPropertyError> {
let schema = release.schema;
let set = model
.get(set_id)
.ok_or(ExactPropertyError::MissingReference {
from: set_id,
to: set_id,
})?;
release.require_exact_slots(set_id, set)?;
let kind = if set.is_type("IFCPROPERTYSET") {
SetKind::Properties
} else if schema.is_a(&set.type_name, "IFCELEMENTQUANTITY") {
SetKind::Quantities
} else if schema.is_a(&set.type_name, "IFCPROPERTYSETDEFINITION") {
SetKind::Predefined
} else {
return Err(ExactPropertyError::UnsupportedDefinition {
entity: set_id,
type_name: set.type_name.clone(),
});
};
Ok((set, kind))
}
pub(super) fn load_source_set(
model: &Model,
release: Release,
source: ExactSource,
id: EntityId,
) -> Result<LoadedSet<'_>, ExactPropertyError> {
match source {
ExactSource::Material(_) => load_material_named(model, release, id),
_ => load_named(model, release, id),
}
}
pub(super) fn load_material_named(
model: &Model,
release: Release,
id: EntityId,
) -> Result<LoadedSet<'_>, ExactPropertyError> {
let schema = release.schema;
let entity = model
.get(id)
.ok_or(ExactPropertyError::MissingReference { from: id, to: id })?;
release.require_exact_slots(id, entity)?;
let type_name = entity.type_name.as_ref();
if !schema.is_a(type_name, "IFCMATERIALPROPERTIES") {
return Err(ExactPropertyError::UnsupportedDefinition {
entity: id,
type_name: entity.type_name.clone(),
});
}
let kind = if material_member_list(release, type_name).is_some() {
SetKind::MaterialProperties
} else {
SetKind::Predefined
};
let (name, named) = predefined_key(release, id, entity)?;
Ok(LoadedSet {
id,
entity,
kind,
name,
named,
})
}
fn material_member_list(release: Release, type_name: &str) -> Option<(usize, &'static str)> {
["Properties", "ExtendedProperties"]
.into_iter()
.find_map(|name| {
release
.attribute(type_name, name)
.map(|(slot, _)| (slot, name))
})
}
pub(super) fn load_named(
model: &Model,
release: Release,
id: EntityId,
) -> Result<LoadedSet<'_>, ExactPropertyError> {
let (entity, kind) = load_set(model, release, id)?;
let (name, named) = match kind {
SetKind::Predefined => predefined_key(release, id, entity)?,
_ => (text_at(id, entity.attributes.get(2), "Name")?, true),
};
Ok(LoadedSet {
id,
entity,
kind,
name,
named,
})
}
impl<'m> LoadedSet<'m> {
pub(super) fn refuse_unselected(
&self,
release: Release,
mut select_property: impl FnMut(&str) -> bool,
) -> Result<(), ExactPropertyError> {
let unnamed_predefined = self.kind == SetKind::Predefined && !self.named;
if unnamed_predefined
&& own_attributes(release, self.entity).any(|(_, name)| select_property(name))
{
return Err(ExactPropertyError::UnsupportedDefinition {
entity: self.id,
type_name: self.entity.type_name.clone(),
});
}
Ok(())
}
pub(super) fn shares_name(
&self,
selected: &mut BTreeMap<&'m str, EntityId>,
) -> Option<EntityId> {
if !self.named {
return None;
}
selected.insert(self.name, self.id)
}
pub(super) fn member_list_is_empty(&self, release: Release) -> bool {
let slot = match self.kind {
SetKind::Properties => slot(release, "IFCPROPERTYSET", "HasProperties"),
SetKind::Quantities => slot(release, "IFCELEMENTQUANTITY", "Quantities"),
SetKind::MaterialProperties => {
let Some((slot, _)) = material_member_list(release, &self.entity.type_name) else {
return false;
};
slot
}
SetKind::Predefined => return false,
};
match self.entity.attributes.get(slot) {
Some(Value::Null) => true,
Some(value) => value.as_list().is_some_and(<[Value]>::is_empty),
None => false,
}
}
pub(super) fn members(
&self,
model: &'m Model,
release: Release,
) -> Result<Vec<(Member, &'m str)>, ExactPropertyError> {
let entities = |members: Vec<(EntityId, &'m str)>| {
members
.into_iter()
.map(|(id, name)| (Member::Entity(id), name))
.collect()
};
Ok(match self.kind {
SetKind::Properties => entities(property_members(
model,
release,
self.id,
self.entity,
(
slot(release, "IFCPROPERTYSET", "HasProperties"),
"HasProperties",
),
)?),
SetKind::MaterialProperties => entities(property_members(
model,
release,
self.id,
self.entity,
material_member_list(release, &self.entity.type_name)
.expect("classified by its member list"),
)?),
SetKind::Quantities => {
entities(quantity_members(model, release, self.id, self.entity)?)
}
SetKind::Predefined => own_attributes(release, self.entity)
.map(|(slot, name)| (Member::Attribute(slot), name))
.collect(),
})
}
pub(super) fn member_id(&self, member: Member) -> EntityId {
match member {
Member::Entity(id) => id,
Member::Attribute(_) => self.id,
}
}
pub(super) fn value(
&self,
model: &Model,
release: Release,
member: Member,
) -> Result<ResolvedValue, ExactPropertyError> {
match (self.kind, member) {
(SetKind::Quantities, Member::Entity(id)) => quantity_value(model, release, id),
(_, Member::Entity(id)) => property_value(model, release, id),
(_, Member::Attribute(slot)) => {
attribute_value(model, release, self.id, self.entity, slot)
}
}
}
pub(super) fn exact(
&self,
source: ExactSource,
member: Member,
resolved: ResolvedValue,
) -> ExactProperty {
ExactProperty {
source,
property_set: Arc::from(self.name),
set_id: self.id,
property_id: self.member_id(member),
value_type: resolved.value_type,
unit_id: resolved.unit_id,
value: resolved.value,
}
}
}
pub(super) fn property_members<'m>(
model: &'m Model,
release: Release,
set_id: EntityId,
set: &Entity,
(list, attribute): (usize, &'static str),
) -> Result<Vec<(EntityId, &'m str)>, ExactPropertyError> {
let schema = release.schema;
let mut members = Vec::new();
for property_id in nonempty_refs_at(set_id, set.attributes.get(list), attribute)? {
let property = model
.get(property_id)
.ok_or(ExactPropertyError::MissingReference {
from: set_id,
to: property_id,
})?;
if schema.entity(property.type_name.as_ref()).is_none() {
return Err(release.not_in_schema(property_id, property.type_name.clone()));
}
if !schema.is_a(property.type_name.as_ref(), "IFCPROPERTY") {
return Err(ExactPropertyError::UnsupportedProperty {
entity: property_id,
type_name: property.type_name.clone(),
});
}
release.require_exact_slots(property_id, property)?;
let name = text_at(property_id, property.attributes.first(), "Name")?;
members.push((property_id, name));
}
Ok(members)
}
pub(super) fn property_value(
model: &Model,
release: Release,
property_id: EntityId,
) -> Result<ResolvedValue, ExactPropertyError> {
let property = model.get(property_id).expect("checked reference");
if is_complex(release, property) {
return complex_value(model, release, property_id, property);
}
simple_property_value(model, release, property_id, property)
}
pub(super) fn simple_property_value(
model: &Model,
release: Release,
property_id: EntityId,
property: &Entity,
) -> Result<ResolvedValue, ExactPropertyError> {
if property.is_type("IFCPROPERTYSINGLEVALUE") {
return exact_property_value(model, release, property_id, property);
}
composite_value(model, release, property_id, property)?.ok_or_else(|| {
ExactPropertyError::UnsupportedProperty {
entity: property_id,
type_name: property.type_name.clone(),
}
})
}
pub(super) fn find_property(
model: &Model,
release: Release,
sets: &[EntityId],
source: ExactSource,
wanted_set: Option<&str>,
wanted_property: &str,
) -> Result<Option<ExactProperty>, ExactPropertyError> {
let mut result: Option<ExactProperty> = None;
let mut matching_sets = BTreeMap::new();
for &set_id in sets {
let set = load_source_set(model, release, source, set_id)?;
if wanted_set.is_some_and(|name| set.name != name) {
set.refuse_unselected(release, |name| name == wanted_property)?;
continue;
}
if let Some(first) = set.shares_name(&mut matching_sets) {
return Err(ExactPropertyError::DuplicateMatchingSets {
source,
first,
second: set_id,
});
}
let mut matching: Option<Member> = None;
for (member, name) in set.members(model, release)? {
if name != wanted_property {
continue;
}
if let Some(first) = matching.replace(member) {
return Err(ExactPropertyError::DuplicateMatchingProperties {
set: set_id,
first: set.member_id(first),
second: set.member_id(member),
});
}
}
if let Some(member) = matching {
let candidate = set.exact(source, member, set.value(model, release, member)?);
if let Some(first) = result.replace(candidate) {
return Err(ExactPropertyError::DuplicateMatchingSets {
source,
first: first.set_id,
second: set_id,
});
}
}
}
Ok(result)
}