pub mod measure;
pub mod symbols;
use std::collections::BTreeMap;
use crate::decoder::EntityDecoder;
pub use measure::{measure_unit, MeasureUnit};
use symbols::{compose_derived, conversion_unit_symbol, si_unit_symbol_and_scale};
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedUnit {
pub symbol: String,
pub si_scale: f64,
}
impl ResolvedUnit {
fn new(symbol: impl Into<String>, si_scale: f64) -> Self {
Self { symbol: symbol.into(), si_scale }
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ProjectUnits {
by_type: BTreeMap<String, ResolvedUnit>,
monetary: Option<ResolvedUnit>,
}
impl ProjectUnits {
pub fn resolve(decoder: &mut EntityDecoder, project_id: u32) -> Self {
let mut units = ProjectUnits::default();
let Ok(project) = decoder.decode_by_id(project_id) else {
return units;
};
if project.ifc_type.as_str() != "IFCPROJECT" {
return units;
}
let Some(units_ref) = project.get(8).and_then(|a| a.as_entity_ref()) else {
return units;
};
let Ok(assignment) = decoder.decode_by_id(units_ref) else {
return units;
};
if assignment.ifc_type.as_str() != "IFCUNITASSIGNMENT" {
return units;
}
let refs: Vec<u32> = match assignment.get(0).and_then(|a| a.as_list()) {
Some(list) => list.iter().filter_map(|a| a.as_entity_ref()).collect(),
None => return units,
};
for unit_ref in refs {
if let Some((unit_type, resolved, monetary)) = resolve_unit_by_ref(decoder, unit_ref) {
if monetary {
units.monetary = Some(resolved);
} else if let Some(t) = unit_type {
units.by_type.entry(t).or_insert(resolved);
}
}
}
units
}
pub fn unit_for_measure(&self, measure_type: &str) -> Option<ResolvedUnit> {
match measure_unit(measure_type)? {
MeasureUnit::Typed { unit_type, default_symbol } => Some(
self.by_type
.get(unit_type)
.cloned()
.unwrap_or_else(|| ResolvedUnit::new(default_symbol, 1.0)),
),
MeasureUnit::Monetary => self.monetary.clone(),
MeasureUnit::Dimensionless => None,
}
}
pub fn resolved_for_unit_type(&self, unit_type: &str) -> Option<&ResolvedUnit> {
self.by_type.get(unit_type)
}
pub fn monetary(&self) -> Option<&ResolvedUnit> {
self.monetary.as_ref()
}
pub fn declared_len(&self) -> usize {
self.by_type.len()
}
}
pub fn resolve_unit_by_ref(
decoder: &mut EntityDecoder,
unit_ref: u32,
) -> Option<(Option<String>, ResolvedUnit, bool)> {
resolve_unit_by_ref_depth(decoder, unit_ref, 0)
}
const MAX_UNIT_RESOLVE_DEPTH: u32 = 16;
fn resolve_unit_by_ref_depth(
decoder: &mut EntityDecoder,
unit_ref: u32,
depth: u32,
) -> Option<(Option<String>, ResolvedUnit, bool)> {
if depth > MAX_UNIT_RESOLVE_DEPTH {
return None;
}
let entity = decoder.decode_by_id(unit_ref).ok()?;
match entity.ifc_type.as_str() {
"IFCSIUNIT" => {
let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
let name = entity.get(3).and_then(|a| a.as_enum())?;
let prefix = entity
.get(2)
.filter(|a| !a.is_null())
.and_then(|a| a.as_enum());
let (symbol, scale) = si_unit_symbol_and_scale(name, prefix)?;
Some((unit_type, ResolvedUnit::new(symbol, scale), false))
}
"IFCCONVERSIONBASEDUNIT" => {
let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
let name = entity.get(2).and_then(|a| a.as_string()).unwrap_or("");
let symbol = conversion_unit_symbol(name);
let conv_ref = entity.get_ref(3);
let scale = conv_ref
.and_then(|r| conversion_factor_scale(decoder, r))
.unwrap_or(1.0);
Some((unit_type, ResolvedUnit::new(symbol, scale), false))
}
"IFCDERIVEDUNIT" => {
let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
let elem_refs: Vec<u32> = entity
.get(0)
.and_then(|a| a.as_list())
.map(|l| l.iter().filter_map(|a| a.as_entity_ref()).collect())
.unwrap_or_default();
let mut parts: Vec<(String, i32)> = Vec::new();
let mut scale = 1.0f64;
for er in elem_refs {
if let Some((sym, unit_scale, exponent)) =
resolve_derived_element(decoder, er, depth)
{
scale *= unit_scale.powi(exponent);
parts.push((sym, exponent));
}
}
let symbol = compose_derived(&parts);
if symbol.is_empty() {
return None;
}
Some((unit_type, ResolvedUnit::new(symbol, scale), false))
}
"IFCMONETARYUNIT" => {
let currency = entity
.get(0)
.and_then(|a| a.as_string().or_else(|| a.as_enum()))
.unwrap_or("");
Some((None, ResolvedUnit::new(currency_symbol(currency), 1.0), true))
}
_ => None,
}
}
fn resolve_derived_element(
decoder: &mut EntityDecoder,
elem_ref: u32,
depth: u32,
) -> Option<(String, f64, i32)> {
let elem = decoder.decode_by_id(elem_ref).ok()?;
if elem.ifc_type.as_str() != "IFCDERIVEDUNITELEMENT" {
return None;
}
let unit_ref = elem.get_ref(0)?;
let exponent = elem.get(1).and_then(|a| a.as_int()).unwrap_or(1) as i32;
let (_ut, resolved, _mon) = resolve_unit_by_ref_depth(decoder, unit_ref, depth + 1)?;
Some((resolved.symbol, resolved.si_scale, exponent))
}
fn conversion_factor_scale(decoder: &mut EntityDecoder, measure_ref: u32) -> Option<f64> {
let measure = decoder.decode_by_id(measure_ref).ok()?;
if measure.ifc_type.as_str() != "IFCMEASUREWITHUNIT" {
return None;
}
let value = measure.get(0).and_then(|a| a.as_float())?;
if !(value.is_finite() && value > 0.0) {
return None;
}
let component_scale = measure
.get_ref(1)
.and_then(|r| {
let comp = decoder.decode_by_id(r).ok()?;
if comp.ifc_type.as_str() == "IFCSIUNIT" {
let name = comp.get(3).and_then(|a| a.as_enum())?;
let prefix = comp.get(2).filter(|a| !a.is_null()).and_then(|a| a.as_enum());
si_unit_symbol_and_scale(name, prefix).map(|(_, s)| s)
} else {
None
}
})
.unwrap_or(1.0);
Some(value * component_scale)
}
fn str_token(s: &str) -> String {
s.trim().trim_matches('.').to_ascii_uppercase()
}
fn currency_symbol(code: &str) -> String {
let c = code.trim().trim_matches('\'').trim_matches('.').trim();
match c.to_ascii_uppercase().as_str() {
"EUR" => "\u{20AC}".to_string(),
"USD" => "$".to_string(),
"GBP" => "\u{00A3}".to_string(),
"JPY" | "CNY" | "RMB" => "\u{00A5}".to_string(),
"" => "".to_string(),
_ => c.to_string(),
}
}
#[cfg(test)]
mod tests;