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, Option<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_declared_unit(decoder, unit_ref) {
if monetary {
units.monetary = 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 } => match self.by_type.get(unit_type) {
Some(declared) => declared.clone(),
None => Some(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)?.as_ref()
}
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)> {
let (unit_type, resolved, monetary) = resolve_declared_unit(decoder, unit_ref)?;
Some((unit_type, resolved?, monetary))
}
type DeclaredUnit = (Option<String>, Option<ResolvedUnit>, bool);
fn resolve_declared_unit(decoder: &mut EntityDecoder, unit_ref: u32) -> Option<DeclaredUnit> {
let mut walk = UnitWalk::default();
let out = resolve_unit_by_ref_walk(decoder, unit_ref, &mut walk);
if walk.was_refused() {
return None;
}
out
}
const MAX_UNIT_RESOLVE_DEPTH: usize = 16;
const MAX_UNIT_RESOLVE_DECODES: u32 = 256;
#[derive(Default)]
struct UnitWalk {
path: Vec<u32>,
decodes: u32,
refused: bool,
}
impl UnitWalk {
fn charge(&mut self) -> bool {
self.decodes += 1;
if self.decodes > MAX_UNIT_RESOLVE_DECODES {
self.refused = true;
return false;
}
true
}
fn was_refused(&self) -> bool {
self.refused
}
}
fn resolve_unit_by_ref_walk(
decoder: &mut EntityDecoder,
unit_ref: u32,
walk: &mut UnitWalk,
) -> Option<DeclaredUnit> {
if walk.path.len() >= MAX_UNIT_RESOLVE_DEPTH || walk.path.contains(&unit_ref) {
walk.refused = true;
return None;
}
if !walk.charge() {
return None;
}
walk.path.push(unit_ref);
let out = resolve_unit_entity(decoder, unit_ref, walk);
walk.path.pop();
out
}
fn resolve_unit_entity(
decoder: &mut EntityDecoder,
unit_ref: u32,
walk: &mut UnitWalk,
) -> Option<DeclaredUnit> {
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, Some(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 scale = entity
.get_ref(3)
.and_then(|r| conversion_factor_scale(decoder, r, walk))
.or_else(|| match unit_type.as_deref() {
Some("LENGTHUNIT") => crate::unit_labels::get_conversion_based_unit_factor(name),
_ => None,
});
let resolved = scale.map(|scale| ResolvedUnit::new(conversion_unit_symbol(name), scale));
Some((unit_type, resolved, 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, walk)
{
scale *= unit_scale.powi(exponent);
parts.push((sym, exponent));
}
}
let symbol = compose_derived(&parts);
if symbol.is_empty() {
return None;
}
Some((unit_type, Some(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, Some(ResolvedUnit::new(currency_symbol(currency), 1.0)), true))
}
_ => None,
}
}
fn resolve_derived_element(
decoder: &mut EntityDecoder,
elem_ref: u32,
walk: &mut UnitWalk,
) -> Option<(String, f64, i32)> {
if !walk.charge() {
return None;
}
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)
.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
let resolved = resolve_unit_by_ref_walk(decoder, unit_ref, walk)?.1?;
Some((resolved.symbol, resolved.si_scale, exponent))
}
fn conversion_factor_scale(
decoder: &mut EntityDecoder,
measure_ref: u32,
walk: &mut UnitWalk,
) -> Option<f64> {
if !walk.charge() {
return None;
}
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 = resolve_unit_by_ref_walk(decoder, measure.get_ref(1)?, walk)?.1?;
Some(value * component.si_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;