use std::sync::Arc;
use ifc_model::{EntityId, Model, Value};
use crate::error::PropertyAnomaly;
use crate::unit::{unit_type, UnitKind};
const NAME: usize = 0;
const DESCRIPTION: usize = 1;
const SIMPLE_UNIT: usize = 2;
const SIMPLE_VALUE: usize = 3;
const SIMPLE_FORMULA: usize = 4;
const COMPLEX_HAS_QUANTITIES: usize = 2;
const COMPLEX_DISCRIMINATION: usize = 3;
const SET_METHOD: usize = 4;
const SET_QUANTITIES: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantityKind {
Length,
Area,
Volume,
Count,
Weight,
Time,
}
impl QuantityKind {
#[must_use]
pub fn from_type_name(name: &str) -> Option<Self> {
Self::from_type(&name.to_ascii_uppercase())
}
#[must_use]
pub fn type_name(self) -> &'static str {
match self {
Self::Length => "IfcQuantityLength",
Self::Area => "IfcQuantityArea",
Self::Volume => "IfcQuantityVolume",
Self::Count => "IfcQuantityCount",
Self::Weight => "IfcQuantityWeight",
Self::Time => "IfcQuantityTime",
}
}
#[must_use]
pub fn measure_type(self) -> &'static str {
match self {
Self::Length => "IfcLengthMeasure",
Self::Area => "IfcAreaMeasure",
Self::Volume => "IfcVolumeMeasure",
Self::Count => "IfcCountMeasure",
Self::Weight => "IfcMassMeasure",
Self::Time => "IfcTimeMeasure",
}
}
fn from_type(name: &str) -> Option<Self> {
Some(match name {
"IFCQUANTITYLENGTH" => Self::Length,
"IFCQUANTITYAREA" => Self::Area,
"IFCQUANTITYVOLUME" => Self::Volume,
"IFCQUANTITYCOUNT" => Self::Count,
"IFCQUANTITYWEIGHT" => Self::Weight,
"IFCQUANTITYTIME" => Self::Time,
_ => return None,
})
}
pub fn required_unit(self) -> Option<&'static str> {
Some(match self {
Self::Length => "LENGTHUNIT",
Self::Area => "AREAUNIT",
Self::Volume => "VOLUMEUNIT",
Self::Weight => "MASSUNIT",
Self::Time => "TIMEUNIT",
Self::Count => return None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Quantity {
Simple {
id: EntityId,
name: Option<Arc<str>>,
description: Option<Arc<str>>,
kind: QuantityKind,
value: f64,
unit: Option<EntityId>,
formula: Option<Arc<str>>,
},
Complex {
id: EntityId,
name: Option<Arc<str>>,
discrimination: Option<Arc<str>>,
quantities: Vec<Quantity>,
},
Unsupported {
id: EntityId,
type_name: Arc<str>,
},
}
impl Quantity {
pub fn id(&self) -> EntityId {
match self {
Self::Simple { id, .. } | Self::Complex { id, .. } | Self::Unsupported { id, .. } => {
*id
}
}
}
pub fn name(&self) -> Option<&str> {
match self {
Self::Simple { name, .. } | Self::Complex { name, .. } => name.as_deref(),
Self::Unsupported { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuantitySet {
pub id: EntityId,
pub name: Option<Arc<str>>,
pub method: Option<Arc<str>>,
pub quantities: Vec<Quantity>,
}
impl QuantitySet {
pub fn quantity(&self, name: &str) -> Option<&Quantity> {
self.quantities.iter().find(|q| q.name() == Some(name))
}
}
const MAX_COMPLEX_DEPTH: usize = 16;
pub fn quantity_set(model: &Model, id: EntityId) -> Option<(QuantitySet, Vec<PropertyAnomaly>)> {
let entity = model.get(id)?;
if !entity.type_name.eq_ignore_ascii_case("IFCELEMENTQUANTITY") {
return None;
}
let mut anomalies = Vec::new();
let quantities = entity
.attributes
.get(SET_QUANTITIES)
.and_then(refs)
.unwrap_or_default()
.into_iter()
.filter_map(|q| read_quantity(model, q, 0, &mut anomalies))
.collect();
Some((
QuantitySet {
id,
name: entity.attributes.get(2).and_then(text),
method: entity.attributes.get(SET_METHOD).and_then(text),
quantities,
},
anomalies,
))
}
fn read_quantity(
model: &Model,
id: EntityId,
depth: usize,
anomalies: &mut Vec<PropertyAnomaly>,
) -> Option<Quantity> {
let entity = model.get(id)?;
let ty = entity.type_name.to_ascii_uppercase();
let name = entity.attributes.get(NAME).and_then(text);
if ty == "IFCPHYSICALCOMPLEXQUANTITY" {
let quantities = if depth >= MAX_COMPLEX_DEPTH {
Vec::new()
} else {
entity
.attributes
.get(COMPLEX_HAS_QUANTITIES)
.and_then(refs)
.unwrap_or_default()
.into_iter()
.filter(|child| *child != id)
.filter_map(|child| read_quantity(model, child, depth + 1, anomalies))
.collect()
};
return Some(Quantity::Complex {
id,
name,
discrimination: entity.attributes.get(COMPLEX_DISCRIMINATION).and_then(text),
quantities,
});
}
let Some(kind) = QuantityKind::from_type(&ty) else {
return Some(Quantity::Unsupported {
id,
type_name: ty.as_str().into(),
});
};
let value = entity
.attributes
.get(SIMPLE_VALUE)
.and_then(|v| v.unwrap_typed().as_f64())?;
let unit = entity.attributes.get(SIMPLE_UNIT).and_then(one_ref);
if value < 0.0 {
anomalies.push(PropertyAnomaly::NegativeQuantity {
quantity: id,
value,
});
}
if let (Some(unit_id), Some(expected)) = (unit, kind.required_unit()) {
if let Some(found) = unit_type(model, unit_id) {
if &*found != expected {
anomalies.push(PropertyAnomaly::QuantityUnitMismatch {
quantity: id,
unit: unit_id,
expected,
found: found.to_string(),
});
}
}
}
Some(Quantity::Simple {
id,
name,
description: entity.attributes.get(DESCRIPTION).and_then(text),
kind,
value,
unit,
formula: entity.attributes.get(SIMPLE_FORMULA).and_then(text),
})
}
pub fn quantity_sets(model: &Model) -> (Vec<QuantitySet>, Vec<PropertyAnomaly>) {
let mut sets = Vec::new();
let mut anomalies = Vec::new();
let mut ids: Vec<_> = model.ids_of_type("IFCELEMENTQUANTITY").to_vec();
ids.sort_unstable();
for id in ids {
if let Some((set, mut found)) = quantity_set(model, id) {
sets.push(set);
anomalies.append(&mut found);
}
}
(sets, anomalies)
}
pub fn stated_unit(model: &Model, quantity: &Quantity) -> Option<UnitKind> {
match quantity {
Quantity::Simple { unit, .. } => {
let id = (*unit)?;
crate::unit::unit(model, id)
}
_ => None,
}
}
fn text(value: &Value) -> Option<Arc<str>> {
match value.unwrap_typed() {
Value::Text(t) => Some(t.clone()),
_ => None,
}
}
fn one_ref(value: &Value) -> Option<EntityId> {
match value.unwrap_typed() {
Value::Ref(id) => Some(*id),
_ => None,
}
}
fn refs(value: &Value) -> Option<Vec<EntityId>> {
match value {
Value::List(items) => Some(items.iter().filter_map(one_ref).collect()),
_ => None,
}
}