use std::collections::BTreeMap;
use ifc_model::{EntityId, Model};
use super::anomaly::SpatialAnomaly;
use super::kind::{Classifier, SpatialKind};
use crate::relation::{Relationship, RelationshipKind};
use ifc_schema::SchemaVersion;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpatialNode {
pub id: EntityId,
pub kind: SpatialKind,
pub parent: Option<EntityId>,
pub children: Vec<EntityId>,
pub elements: Vec<EntityId>,
}
#[derive(Debug, Clone, Default)]
pub struct SpatialTree {
nodes: BTreeMap<EntityId, SpatialNode>,
container_of_element: BTreeMap<EntityId, EntityId>,
pub(super) referenced: BTreeMap<EntityId, Vec<EntityId>>,
pub(super) referenced_in: BTreeMap<EntityId, Vec<EntityId>>,
roots: Vec<EntityId>,
orphans: Vec<EntityId>,
pub(super) dangling: Vec<(EntityId, EntityId)>,
pub(super) anomalies: Vec<SpatialAnomaly>,
release: Option<SchemaVersion>,
}
impl SpatialTree {
#[must_use]
pub fn build(model: &Model) -> Self {
let classifier = Classifier::for_model(model);
let mut tree = Self {
release: classifier.bound_release(),
..Self::default()
};
let mut kinds: BTreeMap<&str, SpatialKind> = BTreeMap::new();
for id in model.ids() {
let Some(entity) = model.get(id) else {
continue;
};
let kind = *kinds
.entry(&entity.type_name)
.or_insert_with(|| classifier.classify(&entity.type_name));
if kind.is_container() {
tree.nodes.insert(
id,
SpatialNode {
id,
kind,
parent: None,
children: Vec::new(),
elements: Vec::new(),
},
);
}
}
for relationship in crate::relation::all(model) {
tree.apply(model, &relationship);
}
tree.apply_references(model);
tree.roots = tree
.nodes
.values()
.filter(|node| node.parent.is_none())
.map(|node| node.id)
.collect();
tree.roots.sort_by_key(|id| {
let kind = tree.nodes[id].kind;
(kind, *id)
});
if tree.roots.len() > 1 {
tree.orphans = tree.roots[1..].to_vec();
}
tree
}
fn apply(&mut self, model: &Model, relationship: &Relationship) {
let Some(parent) = relationship.relating else {
return;
};
if model.get(parent).is_none() {
self.dangling.push((relationship.id, parent));
return;
}
if !self.nodes.contains_key(&parent) {
if relationship.kind == RelationshipKind::ContainedIn {
self.anomalies
.push(SpatialAnomaly::ContainedInNonContainer {
relation: relationship.id,
structure: parent,
});
}
return;
}
let places = matches!(
relationship.kind,
RelationshipKind::Aggregates | RelationshipKind::ContainedIn
);
for &child in &relationship.related {
if model.get(child).is_none() {
self.dangling.push((relationship.id, child));
continue;
}
if !places {
continue;
}
if self.nodes.contains_key(&child) {
if relationship.kind == RelationshipKind::ContainedIn {
continue;
}
let Some(node) = self.nodes.get_mut(&child) else {
continue;
};
match node.parent {
None => {
node.parent = Some(parent);
if let Some(parent_node) = self.nodes.get_mut(&parent) {
parent_node.children.push(child);
}
}
Some(kept) if kept != parent => {
self.anomalies.push(SpatialAnomaly::AggregatedTwice {
child,
kept,
rejected: parent,
relation: relationship.id,
});
}
Some(_) => {}
}
} else {
match self.container_of_element.get(&child) {
None => {
self.container_of_element.insert(child, parent);
if let Some(parent_node) = self.nodes.get_mut(&parent) {
parent_node.elements.push(child);
}
}
Some(&kept) if kept != parent => {
self.anomalies.push(SpatialAnomaly::ContainedTwice {
element: child,
kept,
rejected: parent,
relation: relationship.id,
});
}
Some(_) => {}
}
}
}
}
}
impl SpatialTree {
#[must_use]
pub fn release(&self) -> Option<SchemaVersion> {
self.release
}
#[must_use]
pub fn node(&self, id: EntityId) -> Option<&SpatialNode> {
self.nodes.get(&id)
}
#[must_use]
pub fn roots(&self) -> &[EntityId] {
&self.roots
}
pub fn containers(&self) -> impl Iterator<Item = &SpatialNode> {
self.nodes.values()
}
pub fn of_kind(&self, kind: SpatialKind) -> impl Iterator<Item = &SpatialNode> + '_ {
self.nodes.values().filter(move |node| node.kind == kind)
}
#[must_use]
pub fn elements_of(&self, container: EntityId) -> &[EntityId] {
self.nodes
.get(&container)
.map_or(&[], |node| node.elements.as_slice())
}
#[must_use]
pub fn elements_recursive(&self, container: EntityId) -> Vec<EntityId> {
let mut out = Vec::new();
let mut queue = std::collections::VecDeque::from([container]);
let mut seen = std::collections::BTreeSet::new();
while let Some(current) = queue.pop_front() {
if !seen.insert(current) {
continue;
}
let Some(node) = self.nodes.get(¤t) else {
continue;
};
out.extend_from_slice(&node.elements);
queue.extend(node.children.iter().copied());
}
out
}
#[must_use]
pub fn ancestors(&self, id: EntityId) -> Vec<EntityId> {
let mut out = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let mut current = self.nodes.get(&id).and_then(|node| node.parent);
while let Some(parent) = current {
if !seen.insert(parent) {
break;
}
out.push(parent);
current = self.nodes.get(&parent).and_then(|node| node.parent);
}
out
}
#[must_use]
pub fn container_of(&self, element: EntityId) -> Option<EntityId> {
self.container_of_element.get(&element).copied()
}
#[must_use]
pub fn dangling(&self) -> &[(EntityId, EntityId)] {
&self.dangling
}
#[must_use]
pub fn anomalies(&self) -> &[SpatialAnomaly] {
&self.anomalies
}
#[must_use]
pub fn orphans(&self) -> &[EntityId] {
&self.orphans
}
}