use std::sync::Arc;
use ifc_geometry::{product_world_transform, units, Transform};
use ifc_model::{Entity, EntityId, Model, Value};
use ifc_properties::{
exact_predefined_sets, exact_schema, exact_unit, ExactPredefinedSet, ExactPropertyError,
ExactSource, ExactValue, SchemaVersion,
};
use ifc_schema::{Schema, TypeKind};
use super::{layout, DoorOperationError, DoorOperationType, PanelPosition};
pub(super) struct DoorInputs {
pub(super) operation: DoorOperationType,
pub(super) operation_source: ExactSource,
pub(super) panel_source: ExactSource,
pub(super) overall_width: f64,
pub(super) world: Transform,
pub(super) panels: Vec<Panel>,
}
#[derive(Debug, Clone)]
pub(super) struct Panel {
pub(super) set: EntityId,
pub(super) position: PanelPosition,
pub(super) operation: Arc<str>,
pub(super) width: Option<f64>,
}
pub(super) fn read_door(model: &Model, door: EntityId) -> Result<DoorInputs, DoorOperationError> {
let version = exact_schema(model)?;
let schema = ifc_schema::for_version(version).expect("exact releases are bundled");
let entity = model
.get(door)
.ok_or(ExactPropertyError::MissingReference {
from: door,
to: door,
})?;
let sets = exact_predefined_sets(model, door, "IfcDoorPanelProperties")?;
if !schema.is_a(&entity.type_name, "IFCDOOR") {
return Err(DoorOperationError::NotADoor {
entity: door,
type_name: entity.type_name.clone(),
});
}
let (written, operation_source) = operation_type(model, schema, version, door, entity)?;
let operation = layout::classify(&written)?;
let overall_width = overall_width(model, schema, door, entity)?;
let (panel_source, panels) = governing_panels(door, &sets)?;
let panels = panels.iter().map(panel).collect::<Result<_, _>>()?;
let mut scale = units::resolve(model);
scale.length_to_metres = length_scale(model)?;
let world =
product_world_transform(model, &scale, door).map_err(DoorOperationError::Placement)?;
Ok(DoorInputs {
operation,
operation_source,
panel_source,
overall_width,
world,
panels,
})
}
fn attribute<'m>(
schema: &'static Schema,
entity: &'m Entity,
name: &str,
) -> Option<(&'m Value, &'static str)> {
let (slot, declaration) = schema
.attributes(&entity.type_name)
.into_iter()
.enumerate()
.find(|(_, attribute)| attribute.name.eq_ignore_ascii_case(name))?;
Some((&entity.attributes[slot], declaration.type_name.as_str()))
}
fn enumeration(
schema: &'static Schema,
id: EntityId,
entity: &Entity,
name: &'static str,
) -> Result<Option<Arc<str>>, DoorOperationError> {
let malformed = DoorOperationError::MalformedAttribute {
entity: id,
attribute: name,
};
let Some((value, declared)) = attribute(schema, entity, name) else {
return Ok(None);
};
let members = match schema.type_def(declared).map(|definition| &definition.kind) {
Some(TypeKind::Enumeration(members)) => members,
_ => return Err(malformed),
};
match value {
Value::Null => Ok(None),
Value::Enum(member) if members.iter().any(|m| m.eq_ignore_ascii_case(member)) => {
Ok(Some(member.to_ascii_uppercase().into()))
}
_ => Err(malformed),
}
}
fn operation_type(
model: &Model,
schema: &'static Schema,
version: SchemaVersion,
door: EntityId,
entity: &Entity,
) -> Result<(Arc<str>, ExactSource), DoorOperationError> {
let occurrence = enumeration(schema, door, entity, "OperationType")?;
let typed = match type_object(model, schema, door)? {
Some(type_id) => {
let type_entity = model.get(type_id).expect("validated reference");
let expected = match version {
SchemaVersion::Ifc2x3 => "IFCDOORSTYLE",
_ => "IFCDOORTYPE",
};
if !schema.is_a(&type_entity.type_name, expected) {
return Err(DoorOperationError::UnsupportedTypeObject {
type_object: type_id,
type_name: type_entity.type_name.clone(),
});
}
let value = enumeration(schema, type_id, type_entity, "OperationType")?.ok_or(
DoorOperationError::MalformedAttribute {
entity: type_id,
attribute: "OperationType",
},
)?;
Some((type_id, value))
}
None => None,
};
match (occurrence, typed) {
(Some(occurrence), Some((type_object, type_value))) if occurrence != type_value => {
Err(DoorOperationError::ConflictingOperationType {
occurrence,
type_object,
type_value,
})
}
(Some(occurrence), _) => Ok((occurrence, ExactSource::Occurrence)),
(None, Some((type_object, value))) => Ok((value, ExactSource::Type(type_object))),
(None, None) => Err(DoorOperationError::MissingOperationType { door }),
}
}
fn type_object(
model: &Model,
schema: &'static Schema,
door: EntityId,
) -> Result<Option<EntityId>, DoorOperationError> {
for id in model.ids_of_type("IFCRELDEFINESBYTYPE") {
let relation = model.get(*id).expect("type index is current");
let related = attribute(schema, relation, "RelatedObjects");
let relating = attribute(schema, relation, "RelatingType");
let (Some((Value::List(related), _)), Some((Value::Ref(type_id), _))) = (related, relating)
else {
return Err(DoorOperationError::MalformedAttribute {
entity: *id,
attribute: "RelatingType",
});
};
if related.contains(&Value::Ref(door)) {
return Ok(Some(*type_id));
}
}
Ok(None)
}
fn overall_width(
model: &Model,
schema: &'static Schema,
door: EntityId,
entity: &Entity,
) -> Result<f64, DoorOperationError> {
match attribute(schema, entity, "OverallWidth") {
Some((Value::Null, _)) => Err(DoorOperationError::MissingOverallWidth { door }),
Some((Value::Real(width), _)) if width.is_finite() && *width > 0.0 => {
Ok(width * length_scale(model)?)
}
_ => Err(DoorOperationError::MalformedAttribute {
entity: door,
attribute: "OverallWidth",
}),
}
}
fn length_scale(model: &Model) -> Result<f64, DoorOperationError> {
Ok(exact_unit(model, "IFCPOSITIVELENGTHMEASURE", None)?.scale)
}
fn governing_panels(
door: EntityId,
sets: &[ExactPredefinedSet],
) -> Result<(ExactSource, Vec<&ExactPredefinedSet>), DoorOperationError> {
let source = sets
.iter()
.find(|set| set.source == ExactSource::Occurrence)
.or_else(|| sets.first())
.map(|set| set.source)
.ok_or(DoorOperationError::NoPanelProperties { door })?;
let governing = sets.iter().filter(|set| set.source == source).collect();
Ok((source, governing))
}
fn panel(set: &&ExactPredefinedSet) -> Result<Panel, DoorOperationError> {
let malformed = |attribute| DoorOperationError::MalformedAttribute {
entity: set.set_id,
attribute,
};
let text = |name: &'static str| match set.attribute(name).map(|a| &a.value) {
Some(ExactValue::Enum(value)) => Ok(value.clone()),
_ => Err(malformed(name)),
};
let position = match &*text("PanelPosition")? {
"LEFT" => PanelPosition::Left,
"MIDDLE" => PanelPosition::Middle,
"RIGHT" => PanelPosition::Right,
"NOTDEFINED" => PanelPosition::NotDefined,
_ => return Err(malformed("PanelPosition")),
};
let width = match set.attribute("PanelWidth").map(|a| &a.value) {
Some(ExactValue::Null) => None,
Some(ExactValue::Real(width)) => Some(*width),
_ => return Err(malformed("PanelWidth")),
};
Ok(Panel {
set: set.set_id,
position,
operation: text("PanelOperation")?,
width,
})
}