mod applicability;
mod finding;
mod form;
mod members;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use ifc_model::{Entity, EntityId, Model};
pub use finding::{MeasureRole, TemplateFinding, TemplateReport, UndecidedReason};
use super::layout::Layout;
use super::property_set::{read_set_template, template_links, PropertySetTemplate};
use crate::error::{PropertyAnomaly, TemplateError};
use crate::nesting::Nesting;
use applicability::{applicable, carriers, Verdict};
use form::{attachment, set_entity};
use members::{read_members, Comparison, Member};
pub fn template_deviations(model: &Model) -> Result<TemplateReport, TemplateError> {
let layout = Layout::declared(model)?;
let mut report = TemplateReport::default();
let links = template_links(model, layout);
for &id in model.ids_of_type("IFCRELDEFINESBYTEMPLATE") {
if let Some(rel) = model.get(id) {
layout.check_arity(id, rel, &mut report.anomalies);
}
}
let templates = read_templates(model, layout, &links, &mut report.anomalies);
let templated: BTreeSet<EntityId> = links.keys().copied().collect();
let carried = carriers(model, layout, &templated, &mut report.anomalies);
let no_carriers = BTreeSet::new();
for (&set_id, pairs) in &links {
let relationship = pairs[0].0;
let Some(set) = model.get(set_id) else {
report.anomalies.push(PropertyAnomaly::MissingDefinition {
relationship,
definition: set_id,
});
continue;
};
if !layout.is_a(&set.type_name, "IFCPROPERTYSETDEFINITION") {
report.anomalies.push(PropertyAnomaly::MalformedAttribute {
entity: relationship,
attribute: "RelatedPropertySets",
found: format!("#{} {}", set_id.0, set.type_name),
});
continue;
}
layout.check_arity(set_id, set, &mut report.anomalies);
let members = set_members(model, layout, set_id, set, &mut report.anomalies);
let mut ids: Vec<EntityId> = pairs.iter().map(|(_, template)| *template).collect();
ids.sort_unstable();
ids.dedup();
for template in ids.iter().filter_map(|id| templates.get(id)) {
let pair = Pair {
set_id,
set,
members: members.as_deref(),
template,
objects: carried.get(&set_id).unwrap_or(&no_carriers),
};
pair.check(model, layout, &mut report.findings);
}
}
Ok(report)
}
fn read_templates(
model: &Model,
layout: Layout,
links: &BTreeMap<EntityId, Vec<(EntityId, EntityId)>>,
anomalies: &mut Vec<PropertyAnomaly>,
) -> BTreeMap<EntityId, PropertySetTemplate> {
let mut first: BTreeMap<EntityId, EntityId> = BTreeMap::new();
for &(relationship, template) in links.values().flatten() {
first
.entry(template)
.and_modify(|seen| *seen = (*seen).min(relationship))
.or_insert(relationship);
}
let mut read = BTreeMap::new();
for (id, relationship) in first {
match model.get(id) {
None => anomalies.push(PropertyAnomaly::MissingDefinition {
relationship,
definition: id,
}),
Some(entity) => match read_set_template(model, layout, id, anomalies) {
Some(template) => {
read.insert(id, template);
}
None => anomalies.push(PropertyAnomaly::NotATemplate {
container: relationship,
member: id,
type_name: entity.type_name.to_string(),
}),
},
}
}
read
}
fn set_members<'m>(
model: &'m Model,
layout: Layout,
id: EntityId,
set: &'m Entity,
anomalies: &mut Vec<PropertyAnomaly>,
) -> Option<Vec<Member<'m>>> {
let attribute = if layout.is_a(&set.type_name, "IFCPROPERTYSET") {
"HasProperties"
} else if layout.is_a(&set.type_name, "IFCELEMENTQUANTITY") {
"Quantities"
} else {
return None;
};
let mut nesting = Nesting::new(anomalies);
let list = layout.get(set, attribute);
Some(read_members(
model,
layout,
id,
list,
attribute,
&mut nesting,
))
}
struct Pair<'a, 'm> {
set_id: EntityId,
set: &'m Entity,
members: Option<&'a [Member<'m>]>,
template: &'a PropertySetTemplate,
objects: &'a BTreeSet<EntityId>,
}
impl Pair<'_, '_> {
fn check(&self, model: &Model, layout: Layout, findings: &mut Vec<TemplateFinding>) {
let set = self.set_id;
let template = self.template.id;
let template_type = self.template.template_type.as_ref();
let undecided = |subject, reason| TemplateFinding::Undecided {
set,
template,
subject,
reason,
};
let rule = match template_type {
None => Some(form::Attachment::Any),
Some(token) => {
let rule = attachment(layout, token);
if rule.is_none() {
let value = token.clone();
findings.push(undecided(
template,
UndecidedReason::UnknownTemplateType { value },
));
}
rule
}
};
if let (Some(_), Some(token)) = (rule, template_type) {
if let Some(expected) = set_entity(token) {
if !layout.is_a(&self.set.type_name, expected) {
findings.push(TemplateFinding::WrongSetKind {
set,
template,
template_type: token.clone(),
expected,
found: self.set.type_name.clone(),
});
}
}
}
match self.members {
Some(members) => Comparison {
model,
layout,
set,
template,
findings: &mut *findings,
}
.members(set, members, &self.template.properties),
None => findings.push(undecided(
set,
UndecidedReason::PredefinedSet {
found: self.set.type_name.clone(),
},
)),
}
for &object in self.objects {
let entity = model.get(object).expect("carriers are in the model");
if let (Some(rule), Some(token)) = (rule, template_type) {
if !rule.admits(layout, &entity.type_name) {
findings.push(TemplateFinding::WrongAttachment {
set,
template,
object,
template_type: token.clone(),
found: entity.type_name.clone(),
});
}
}
let Some(applicable_entity) = &self.template.applicable_entity else {
continue;
};
match applicable(layout, entity, applicable_entity) {
Verdict::Admits => {}
Verdict::Excludes => findings.push(TemplateFinding::OutsideApplicableEntity {
set,
template,
object,
found: entity.type_name.clone(),
applicable_entity: Arc::clone(applicable_entity),
}),
Verdict::Undecided(reason) => findings.push(undecided(object, reason)),
}
}
}
}