ifcviewer: port unit utilities to C++ in Unit.{h,cpp}

Mirrors selected helpers from ifcopenshell.util.unit: SI prefix
multipliers, the conversion-based-unit table (foot/inch/etc -> SI
metres), siScaleFromNamedUnit (walks IfcConversionBasedUnit chains
down to IfcSIUnit), getUnitAssignment / getProjectUnit /
calculateUnitScale, and convert / convertUnit.  Lives in
src/ifcviewer/ for now alongside Geolocation; will move out when
ifcopenshell.util is ported to C++.

Needed by upcoming Geolocation fix (e/n/h on IfcMapConversion are
in MapUnit, must be converted to metres for the meter-by-default
iterator output) and by the federation module (display-unit
conversion when the user changes the federation unit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-01 12:12:29 +10:00
parent a2db0a68a4
commit 0d3849737c
2 changed files with 418 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Unit.h"
#include "../ifcparse/file.h"
#include "../ifcparse/instance_data.h"
#include "../ifcparse/schema.h"
#include <algorithm>
#include <cctype>
const std::unordered_map<std::string, double> kSiPrefixes = {
{ "EXA", 1e18 },
{ "PETA", 1e15 },
{ "TERA", 1e12 },
{ "GIGA", 1e9 },
{ "MEGA", 1e6 },
{ "KILO", 1e3 },
{ "HECTO", 1e2 },
{ "DECA", 1e1 },
{ "DECI", 1e-1 },
{ "CENTI", 1e-2 },
{ "MILLI", 1e-3 },
{ "MICRO", 1e-6 },
{ "NANO", 1e-9 },
{ "PICO", 1e-12 },
{ "FEMTO", 1e-15 },
{ "ATTO", 1e-18 },
};
const std::unordered_map<std::string, std::string> kSiPrefixSymbols = {
{ "EXA", "E" },
{ "PETA", "P" },
{ "TERA", "T" },
{ "GIGA", "G" },
{ "MEGA", "M" },
{ "KILO", "k" },
{ "HECTO", "h" },
{ "DECA", "da" },
{ "DECI", "d" },
{ "CENTI", "c" },
{ "MILLI", "m" },
{ "MICRO", "\xCE\xBC" }, // μ (UTF-8)
{ "NANO", "n" },
{ "PICO", "p" },
{ "FEMTO", "f" },
{ "ATTO", "a" },
};
const std::unordered_map<std::string, double> kSiConversions = {
{ "thou", 0.0000254 },
{ "inch", 0.0254 },
{ "foot", 0.3048 },
{ "yard", 0.914 },
{ "mile", 1609.0 },
{ "square thou", 6.4516e-10 },
{ "square inch", 0.0006452 },
{ "square foot", 0.09290304 },
{ "square yard", 0.83612736 },
{ "acre", 4046.86 },
{ "square mile", 2588881.0 },
{ "cubic thou", 1.6387064e-14 },
{ "cubic inch", 0.00001639 },
{ "cubic foot", 0.02831684671168849 },
{ "cubic yard", 0.7636 },
{ "cubic mile", 4165509529.0 },
{ "litre", 0.001 },
{ "fluid ounce uk", 0.0000284130625 },
{ "fluid ounce us", 0.00002957353 },
{ "pint uk", 0.000568 },
{ "pint us", 0.000473 },
{ "gallon uk", 0.004546 },
{ "gallon us", 0.003785 },
{ "degree", 0.0174532925199433 }, // pi / 180
{ "ounce", 0.02835 },
{ "pound", 0.454 },
{ "ton uk", 1016.0469088 },
{ "ton us", 907.18474 },
{ "tonne", 1000.0 },
{ "lbf", 4.4482216153 },
{ "kip", 4448.2216153 },
{ "psi", 6894.7572932 },
{ "ksi", 6894757.2932 },
{ "minute", 60.0 },
{ "hour", 3600.0 },
{ "day", 86400.0 },
{ "btu", 1055.056 },
{ "fahrenheit", 1.8 },
};
const std::unordered_map<std::string, std::string> kImperialTypes = {
{ "thou", "LENGTHUNIT" }, { "inch", "LENGTHUNIT" }, { "foot", "LENGTHUNIT" },
{ "yard", "LENGTHUNIT" }, { "mile", "LENGTHUNIT" },
{ "square thou", "AREAUNIT" }, { "square inch", "AREAUNIT" },
{ "square foot", "AREAUNIT" }, { "square yard", "AREAUNIT" },
{ "acre", "AREAUNIT" }, { "square mile", "AREAUNIT" },
{ "cubic thou", "VOLUMEUNIT" }, { "cubic inch", "VOLUMEUNIT" },
{ "cubic foot", "VOLUMEUNIT" }, { "cubic yard", "VOLUMEUNIT" },
{ "cubic mile", "VOLUMEUNIT" }, { "litre", "VOLUMEUNIT" },
{ "fluid ounce uk", "VOLUMEUNIT" }, { "fluid ounce us", "VOLUMEUNIT" },
{ "pint uk", "VOLUMEUNIT" }, { "pint us", "VOLUMEUNIT" },
{ "gallon uk", "VOLUMEUNIT" }, { "gallon us", "VOLUMEUNIT" },
{ "degree", "PLANEANGLEUNIT" },
{ "ounce", "MASSUNIT" }, { "pound", "MASSUNIT" },
{ "ton uk", "MASSUNIT" }, { "ton us", "MASSUNIT" }, { "tonne", "MASSUNIT" },
{ "lbf", "FORCEUNIT" }, { "kip", "FORCEUNIT" },
{ "psi", "PRESSUREUNIT" }, { "ksi", "PRESSUREUNIT" },
{ "minute", "TIMEUNIT" }, { "hour", "TIMEUNIT" }, { "day", "TIMEUNIT" },
{ "btu", "ENERGYUNIT" },
{ "fahrenheit", "THERMODYNAMICTEMPERATUREUNIT" },
};
const std::unordered_map<std::string, std::string> kUnitSymbols = {
// SI base / derived
{ "CUBIC_METRE", "m3" },
{ "GRAM", "g" },
{ "SECOND", "s" },
{ "SQUARE_METRE", "m2" },
{ "METRE", "m" },
{ "NEWTON", "N" },
{ "PASCAL", "Pa" },
// Conversion-based
{ "pound-force", "lbf" },
{ "pound-force per square inch", "psi" },
{ "thou", "th" }, { "inch", "in" }, { "foot", "ft" },
{ "yard", "yd" }, { "mile", "mi" },
{ "square thou", "th2" }, { "square inch", "in2" },
{ "square foot", "ft2" }, { "square yard", "yd2" },
{ "acre", "ac" }, { "square mile", "mi2" },
{ "cubic thou", "th3" }, { "cubic inch", "in3" },
{ "cubic foot", "ft3" }, { "cubic yard", "yd3" },
{ "cubic mile", "mi3" }, { "litre", "L" },
{ "fluid ounce uk", "fl oz" }, { "fluid ounce us", "fl oz" },
{ "pint uk", "pt" }, { "pint us", "pt" },
{ "gallon uk", "gal" }, { "gallon us", "gal" },
{ "degree", "\xC2\xB0" }, // °
{ "ounce", "oz" }, { "pound", "lb" },
{ "ton uk", "ton" }, { "ton us", "ton" }, { "tonne", "t" },
{ "lbf", "lbf" }, { "kip", "kip" },
{ "psi", "psi" }, { "ksi", "ksi" },
{ "minute", "min" }, { "hour", "hr" }, { "day", "day" },
{ "btu", "btu" },
{ "fahrenheit", "\xC2\xB0\x46" }, // °F
};
namespace {
std::string toLower(const std::string& s) {
std::string r;
r.resize(s.size());
std::transform(s.begin(), s.end(), r.begin(),
[](unsigned char c) { return std::tolower(c); });
return r;
}
// Pull an enumeration string off an attribute_value, or "" if null/invalid.
std::string enumString(const attribute_value& av) {
if (av.isNull()) return {};
if (av.type() != ifcopenshell::Argument_ENUMERATION) return {};
enumeration_reference er = av;
return std::string(er.value() ? er.value() : "");
}
} // namespace
double getPrefixMultiplier(const std::string& prefix) {
if (prefix.empty()) return 1.0;
auto it = kSiPrefixes.find(prefix);
return (it == kSiPrefixes.end()) ? 1.0 : it->second;
}
std::optional<double> siScaleFromNamedUnit(express::Base unit) {
double scale = 1.0;
while (unit && unit.declaration().is("IfcConversionBasedUnit")) {
auto e = unit.as<express::Entity>();
// Fast path: name in si_conversions table — matches python.
std::string name;
auto name_attr = e.get("Name");
if (!name_attr.isNull()) name = (std::string) name_attr;
if (auto it = kSiConversions.find(toLower(name));
it != kSiConversions.end()) {
return scale * it->second;
}
// Otherwise walk the ConversionFactor chain.
auto cf_attr = e.get("ConversionFactor");
if (cf_attr.isNull()) return std::nullopt;
express::Base cf = cf_attr;
auto cf_e = cf.as<express::Entity>();
auto vc_attr = cf_e.get("ValueComponent");
if (vc_attr.isNull()) return std::nullopt;
express::Base vc = vc_attr;
// ValueComponent is an IfcValue SELECT wrapping a measure.
scale *= (double) vc.get_attribute_value(0);
auto uc_attr = cf_e.get("UnitComponent");
if (uc_attr.isNull()) return std::nullopt;
unit = (express::Base) uc_attr;
}
if (unit && unit.declaration().is("IfcSIUnit")) {
auto e = unit.as<express::Entity>();
const std::string prefix = enumString(e.get("Prefix"));
const std::string name = enumString(e.get("Name"));
double m = getPrefixMultiplier(prefix);
// SQUARE_/CUBIC_-prefixed SI names: prefix multiplier squared/cubed.
if (name.find("SQUARE") != std::string::npos) {
m *= getPrefixMultiplier(prefix);
} else if (name.find("CUBIC") != std::string::npos) {
m *= getPrefixMultiplier(prefix);
m *= getPrefixMultiplier(prefix);
}
return scale * m;
}
if (unit && unit.declaration().is("IfcContextDependentUnit")) {
// No conversion to SI is possible for a context-dependent unit.
return std::nullopt;
}
return scale;
}
std::optional<express::Base> getUnitAssignment(ifcopenshell::file* ifc_file) {
auto projects = ifc_file->instances_by_type("IfcProject");
if (projects.empty()) return std::nullopt;
auto ua_attr = projects[0].as<express::Entity>().get("UnitsInContext");
if (ua_attr.isNull()) return std::nullopt;
return (express::Base) ua_attr;
}
std::optional<express::Base> getProjectUnit(ifcopenshell::file* ifc_file,
const std::string& unit_type) {
auto ua = getUnitAssignment(ifc_file);
if (!ua) return std::nullopt;
auto units_attr = ua->as<express::Entity>().get("Units");
if (units_attr.isNull()) return std::nullopt;
std::vector<express::Base> units = units_attr;
for (const auto& unit : units) {
// IfcMonetaryUnit has no UnitType — guard via declaration check.
if (!unit.declaration().is("IfcNamedUnit") &&
!unit.declaration().is("IfcDerivedUnit")) {
continue;
}
auto ut = unit.as<express::Entity>().get("UnitType");
if (enumString(ut) == unit_type) return unit;
}
return std::nullopt;
}
double calculateUnitScale(ifcopenshell::file* ifc_file,
const std::string& unit_type) {
auto unit = getProjectUnit(ifc_file, unit_type);
if (!unit) return 1.0;
auto scale = siScaleFromNamedUnit(*unit);
return scale.value_or(1.0);
}
double convert(double value,
const std::string& from_prefix, const std::string& from_unit,
const std::string& to_prefix, const std::string& to_unit) {
const std::string fl = toLower(from_unit);
const std::string tl = toLower(to_unit);
if (auto it = kSiConversions.find(fl); it != kSiConversions.end()) {
value *= it->second;
} else if (!from_prefix.empty()) {
value *= getPrefixMultiplier(from_prefix);
if (from_unit.find("SQUARE") != std::string::npos) {
value *= getPrefixMultiplier(from_prefix);
} else if (from_unit.find("CUBIC") != std::string::npos) {
value *= getPrefixMultiplier(from_prefix);
value *= getPrefixMultiplier(from_prefix);
}
}
if (auto it = kSiConversions.find(tl); it != kSiConversions.end()) {
return value * (1.0 / it->second);
} else if (!to_prefix.empty()) {
value *= 1.0 / getPrefixMultiplier(to_prefix);
// NB: python ifcopenshell.util.unit.convert checks `from_unit` (not
// `to_unit`) here. Mirrored for parity — from_unit and to_unit are
// always the same dimension in valid calls, so behaviour is the same.
if (from_unit.find("SQUARE") != std::string::npos) {
value *= 1.0 / getPrefixMultiplier(to_prefix);
} else if (from_unit.find("CUBIC") != std::string::npos) {
value *= 1.0 / getPrefixMultiplier(to_prefix);
value *= 1.0 / getPrefixMultiplier(to_prefix);
}
}
return value;
}
double convertUnit(double value, express::Base from_unit, express::Base to_unit) {
auto pull = [](express::Base u, std::string& prefix, std::string& name) {
auto e = u.as<express::Entity>();
if (u.declaration().is("IfcSIUnit")) {
prefix = enumString(e.get("Prefix"));
name = enumString(e.get("Name"));
} else {
// IfcConversionBasedUnit / IfcContextDependentUnit: no Prefix,
// Name is a string attribute.
prefix.clear();
auto name_attr = e.get("Name");
name = name_attr.isNull() ? "" : (std::string) name_attr;
}
};
std::string fp, fn, tp, tn;
pull(from_unit, fp, fn);
pull(to_unit, tp, tn);
return convert(value, fp, fn, tp, tn);
}
+89
View File
@@ -0,0 +1,89 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// Port of selected helpers from
// src/ifcopenshell-python/ifcopenshell/util/unit.py. Lives in src/ifcviewer/
// for now alongside Geolocation; will move out once ifcopenshell.util is
// ported to C++.
#ifndef UNIT_H
#define UNIT_H
#include "../ifcparse/express.h"
#include <optional>
#include <string>
#include <unordered_map>
namespace ifcopenshell { class file; }
// SI prefix multipliers, e.g. "MILLI" -> 1e-3. Empty key not present;
// callers should pass an empty prefix string for "no prefix".
extern const std::unordered_map<std::string, double> kSiPrefixes;
// SI prefix display symbols, e.g. "MILLI" -> "m".
extern const std::unordered_map<std::string, std::string> kSiPrefixSymbols;
// Conversion-based unit name (lowercase, IFC convention) -> SI base scale.
// e.g. "foot" -> 0.3048, "square foot" -> 0.09290304.
extern const std::unordered_map<std::string, double> kSiConversions;
// Conversion-based unit name -> IFC unit type, e.g. "foot" -> "LENGTHUNIT".
extern const std::unordered_map<std::string, std::string> kImperialTypes;
// Display symbol per unit name. Covers IfcSIUnit names ("METRE" -> "m") and
// IfcConversionBasedUnit names ("foot" -> "ft").
extern const std::unordered_map<std::string, std::string> kUnitSymbols;
// Returns the multiplier for an SI prefix. Empty string returns 1.0.
double getPrefixMultiplier(const std::string& prefix);
// Returns the SI scale for an IfcNamedUnit such that
// value_in_unit * scale == value_in_si_base
// Walks IfcConversionBasedUnit chains down to IfcSIUnit. Returns nullopt
// when the chain bottoms out in IfcContextDependentUnit (cannot convert).
std::optional<double> siScaleFromNamedUnit(express::Base named_unit);
// IfcProject.UnitsInContext (the IfcUnitAssignment). Returns nullopt if
// the file has no project or no assignment.
std::optional<express::Base> getUnitAssignment(ifcopenshell::file* ifc_file);
// First unit in the project's IfcUnitAssignment matching `unit_type`
// (e.g. "LENGTHUNIT"). Returns nullopt if not found.
std::optional<express::Base> getProjectUnit(ifcopenshell::file* ifc_file,
const std::string& unit_type);
// Project unit -> SI base scale (e.g. project in mm => 0.001). Defaults
// to 1.0 when no project unit of the requested type is set.
double calculateUnitScale(ifcopenshell::file* ifc_file,
const std::string& unit_type = "LENGTHUNIT");
// Convert between two units identified by name + optional SI prefix.
// SQUARE_/CUBIC_ prefixed SI names get the prefix multiplier squared/cubed
// (matches python ifcopenshell.util.unit.convert).
double convert(double value,
const std::string& from_prefix, const std::string& from_unit,
const std::string& to_prefix, const std::string& to_unit);
// Convert between two IfcNamedUnit entities. Pulls Name and Prefix off each
// and delegates to convert(). IfcConversionBasedUnit names that don't appear
// in kSiConversions return the value unchanged.
double convertUnit(double value, express::Base from_unit, express::Base to_unit);
#endif // UNIT_H