use std::sync::Arc;
use ifc_model::{EntityId, Model, Value};
const NAMED_UNIT_TYPE: usize = 1;
const SI_PREFIX: usize = 2;
const SI_NAME: usize = 3;
const CONVERSION_NAME: usize = 2;
const CONVERSION_FACTOR: usize = 3;
const CONVERSION_OFFSET: usize = 4;
const MEASURE_VALUE: usize = 0;
const MEASURE_UNIT: usize = 1;
const DERIVED_ELEMENTS: usize = 0;
const DERIVED_TYPE: usize = 1;
const DERIVED_ELEMENT_UNIT: usize = 0;
const DERIVED_ELEMENT_EXPONENT: usize = 1;
const MONETARY_CURRENCY: usize = 0;
const ASSIGNMENT_UNITS: usize = 0;
#[derive(Debug, Clone, PartialEq)]
pub enum UnitKind {
Si {
unit_type: Arc<str>,
name: Arc<str>,
prefix: Option<Arc<str>>,
prefix_exponent: Option<i32>,
},
Conversion {
unit_type: Arc<str>,
name: Option<Arc<str>>,
factor: Option<f64>,
factor_unit: Option<EntityId>,
offset: Option<f64>,
},
Derived {
unit_type: Arc<str>,
elements: Vec<(EntityId, i64)>,
},
Monetary {
currency: Option<Arc<str>>,
},
ContextDependent {
unit_type: Arc<str>,
},
}
impl UnitKind {
pub fn unit_type(&self) -> Option<&str> {
match self {
Self::Si { unit_type, .. }
| Self::Conversion { unit_type, .. }
| Self::ContextDependent { unit_type } => Some(unit_type),
Self::Derived { unit_type, .. } => Some(unit_type),
Self::Monetary { .. } => None,
}
}
pub fn si_scale(&self) -> Option<f64> {
match self {
Self::Si {
name,
prefix_exponent,
..
} => prefix_exponent.map(|exponent| 10f64.powi(exponent * super::si::name_power(name))),
_ => None,
}
}
}
pub fn prefix_exponent(prefix: &str) -> Option<i32> {
Some(match prefix {
"EXA" => 18,
"PETA" => 15,
"TERA" => 12,
"GIGA" => 9,
"MEGA" => 6,
"KILO" => 3,
"HECTO" => 2,
"DECA" => 1,
"DECI" => -1,
"CENTI" => -2,
"MILLI" => -3,
"MICRO" => -6,
"NANO" => -9,
"PICO" => -12,
"FEMTO" => -15,
"ATTO" => -18,
_ => return None,
})
}
pub fn unit(model: &Model, id: EntityId) -> Option<UnitKind> {
let entity = model.get(id)?;
let ty = entity.type_name.to_ascii_uppercase();
match ty.as_str() {
"IFCSIUNIT" => {
let prefix = entity.attributes.get(SI_PREFIX).and_then(enum_text);
Some(UnitKind::Si {
unit_type: entity
.attributes
.get(NAMED_UNIT_TYPE)
.and_then(enum_text)
.unwrap_or_else(|| "".into()),
name: entity
.attributes
.get(SI_NAME)
.and_then(enum_text)
.unwrap_or_else(|| "".into()),
prefix_exponent: prefix.as_deref().map_or(Some(0), prefix_exponent),
prefix,
})
}
"IFCCONVERSIONBASEDUNIT" | "IFCCONVERSIONBASEDUNITWITHOFFSET" => {
let factor_entity = entity.attributes.get(CONVERSION_FACTOR).and_then(one_ref);
let (factor, factor_unit) = match factor_entity.and_then(|f| model.get(f)) {
Some(measure) => (
measure
.attributes
.get(MEASURE_VALUE)
.and_then(|v| v.unwrap_typed().as_f64()),
measure.attributes.get(MEASURE_UNIT).and_then(one_ref),
),
None => (None, None),
};
Some(UnitKind::Conversion {
unit_type: entity
.attributes
.get(NAMED_UNIT_TYPE)
.and_then(enum_text)
.unwrap_or_else(|| "".into()),
name: entity.attributes.get(CONVERSION_NAME).and_then(text),
factor,
factor_unit,
offset: if ty == "IFCCONVERSIONBASEDUNITWITHOFFSET" {
entity
.attributes
.get(CONVERSION_OFFSET)
.and_then(|v| v.unwrap_typed().as_f64())
} else {
None
},
})
}
"IFCDERIVEDUNIT" => {
let elements = entity
.attributes
.get(DERIVED_ELEMENTS)
.and_then(refs)
.unwrap_or_default()
.into_iter()
.filter_map(|e| {
let element = model.get(e)?;
let unit = element
.attributes
.get(DERIVED_ELEMENT_UNIT)
.and_then(one_ref)?;
let exponent = match element
.attributes
.get(DERIVED_ELEMENT_EXPONENT)?
.unwrap_typed()
{
Value::Integer(v) => *v,
Value::Real(v) => *v as i64,
_ => return None,
};
Some((unit, exponent))
})
.collect();
Some(UnitKind::Derived {
unit_type: entity
.attributes
.get(DERIVED_TYPE)
.and_then(enum_text)
.unwrap_or_else(|| "".into()),
elements,
})
}
"IFCMONETARYUNIT" => Some(UnitKind::Monetary {
currency: entity.attributes.get(MONETARY_CURRENCY).and_then(text),
}),
"IFCCONTEXTDEPENDENTUNIT" => Some(UnitKind::ContextDependent {
unit_type: entity
.attributes
.get(NAMED_UNIT_TYPE)
.and_then(enum_text)
.unwrap_or_else(|| "".into()),
}),
_ => None,
}
}
pub fn unit_type(model: &Model, id: EntityId) -> Option<Arc<str>> {
let entity = model.get(id)?;
let ty = entity.type_name.to_ascii_uppercase();
let slot = if ty == "IFCDERIVEDUNIT" {
DERIVED_TYPE
} else {
NAMED_UNIT_TYPE
};
entity.attributes.get(slot).and_then(enum_text)
}
pub fn project_units(model: &Model) -> Vec<(EntityId, UnitKind)> {
let mut out = Vec::new();
let mut ids: Vec<_> = model.ids_of_type("IFCUNITASSIGNMENT").to_vec();
ids.sort_unstable();
for id in ids {
let Some(assignment) = model.get(id) else {
continue;
};
for unit_id in assignment
.attributes
.get(ASSIGNMENT_UNITS)
.and_then(refs)
.unwrap_or_default()
{
if let Some(kind) = unit(model, unit_id) {
out.push((unit_id, kind));
}
}
}
out
}
pub fn project_unit_for(model: &Model, unit_type_name: &str) -> Option<(EntityId, UnitKind)> {
project_units(model)
.into_iter()
.find(|(_, kind)| kind.unit_type() == Some(unit_type_name))
}
fn text(value: &Value) -> Option<Arc<str>> {
match value.unwrap_typed() {
Value::Text(t) => Some(t.clone()),
_ => None,
}
}
fn enum_text(value: &Value) -> Option<Arc<str>> {
match value.unwrap_typed() {
Value::Enum(t) | 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,
}
}