use std::collections::{BTreeMap, BTreeSet};
use ifc_model::{EntityId, Model, Value};
use crate::error::{NotInSchema, SchemaGap, SchemaResolutionError, SystemAnomaly};
use crate::release::{self, Release};
const ZONE: &str = "IFCZONE";
const ASSIGNS: &str = "IFCRELASSIGNSTOGROUP";
const ZONE_MEMBER_TYPES: [&str; 3] = ["IFCZONE", "IFCSPACE", "IFCSPATIALZONE"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Zone {
pub id: EntityId,
pub name: Option<String>,
pub long_name: Option<String>,
pub members: Vec<EntityId>,
}
fn refs(value: Option<&Value>) -> Vec<EntityId> {
match value {
Some(Value::List(items)) => items
.iter()
.filter_map(|v| match v {
Value::Ref(id) => Some(*id),
_ => None,
})
.collect(),
Some(Value::Ref(id)) => vec![*id],
_ => Vec::new(),
}
}
fn text(model: &Model, release: Release, id: EntityId, attribute: &str) -> Option<String> {
let slot = release.slot(ZONE, attribute)?;
match model.get(id)?.attributes.get(slot)? {
Value::Text(t) => Some(t.to_string()),
_ => None,
}
}
pub fn zones(model: &Model) -> (Vec<Zone>, Vec<SystemAnomaly>) {
zones_in(model, release::resolve_zones_or_ifc4(model))
}
pub fn try_zones(model: &Model) -> Result<(Vec<Zone>, Vec<SystemAnomaly>), SchemaResolutionError> {
let release = match release::resolve_zones(model) {
Err(SchemaResolutionError::MissingSchema) => release::ifc4(),
resolved => resolved?,
};
Ok(zones_in(model, release))
}
fn zones_in(model: &Model, release: Release) -> (Vec<Zone>, Vec<SystemAnomaly>) {
let mut anomalies = Vec::new();
let mut zone_ids = BTreeSet::new();
for (type_name, _) in model.type_histogram() {
if release.is_a(type_name, ZONE) {
zone_ids.extend(model.ids_of_type(type_name).iter().copied());
}
}
let mut members: BTreeMap<EntityId, Vec<EntityId>> = BTreeMap::new();
let group_slot = release
.slot(ASSIGNS, "RelatingGroup")
.expect("every bundled release declares IfcRelAssignsToGroup.RelatingGroup");
let members_slot = release
.slot(ASSIGNS, "RelatedObjects")
.expect("every bundled release declares IfcRelAssigns.RelatedObjects");
for &relation in model.ids_of_type(ASSIGNS) {
let Some(entity) = model.get(relation) else {
continue;
};
let group = match entity.attributes.get(group_slot) {
Some(Value::Ref(id)) => *id,
_ => continue,
};
if !zone_ids.contains(&group) {
continue;
}
for member in refs(entity.attributes.get(members_slot)) {
let Some(member_entity) = model.get(member) else {
anomalies.push(SystemAnomaly::Dangling {
relation,
missing: member,
});
continue;
};
let type_name = member_entity.type_name.to_ascii_uppercase();
let permitted = ZONE_MEMBER_TYPES
.iter()
.any(|allowed| release.is_a(&type_name, allowed));
if permitted {
members.entry(group).or_default().push(member);
} else {
anomalies.push(SystemAnomaly::ZoneMemberNotSpatial {
relation,
zone: group,
member,
type_name: member_entity.type_name.to_string(),
});
}
}
}
let zones = zone_ids
.into_iter()
.map(|id| {
let mut member_ids = members.remove(&id).unwrap_or_default();
member_ids.sort_unstable();
member_ids.dedup();
Zone {
id,
name: text(model, release, id, "Name"),
long_name: text(model, release, id, "LongName"),
members: member_ids,
}
})
.collect();
(zones, anomalies)
}
pub fn long_name_of(model: &Model, zone: EntityId) -> Result<Option<String>, SchemaGap> {
let release: Release = release::resolve_zones(model)?;
if release.slot(ZONE, "LongName").is_none() {
return Err(SchemaGap::NotInSchema(NotInSchema {
entity: zone,
schema: release.version,
}));
}
Ok(text(model, release, zone, "LongName"))
}