From 5c75cc6a39d3e8d9a137d9a8d571ffb1bcbaf4c4 Mon Sep 17 00:00:00 2001 From: krande Date: Sat, 16 Dec 2023 13:37:38 +0100 Subject: [PATCH] initial proposal for updating units conversion --- .../ifcopenshell/util/unit.py | 97 +++++++++++++++++-- .../ifcpatch/recipes/ConvertLengthUnit.py | 42 +------- 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 28bc705f00..300f3a0501 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell from math import pi from fractions import Fraction @@ -214,12 +215,10 @@ si_conversions = { "fahrenheit": 1.8, } - si_offsets = { "fahrenheit": -459.67, } - imperial_types = { "thou": "LENGTHUNIT", "inch": "LENGTHUNIT", @@ -260,7 +259,6 @@ imperial_types = { "fahrenheit": "THERMODYNAMICTEMPERATUREUNIT", } - prefix_symbols = { "EXA": "E", "PETA": "P", @@ -558,13 +556,13 @@ def calculate_unit_scale(ifc_file): def format_length( - value, - precision, - decimal_places=2, - suppress_zero_inches=True, - unit_system="imperial", - input_unit="foot", - output_unit="foot", + value, + precision, + decimal_places=2, + suppress_zero_inches=True, + unit_system="imperial", + input_unit="foot", + output_unit="foot", ): """Formats a length for readability and imperial formatting @@ -624,3 +622,82 @@ def format_length( elif unit_system == "metric": rounded_val = round(value / precision) * precision return f"{rounded_val:.{decimal_places}f}" + + +def get_base_type_name( + content_type: ifcopenshell.ifcopenshell_wrapper.named_type | ifcopenshell.ifcopenshell_wrapper.type_declaration) -> ifcopenshell.ifcopenshell_wrapper.type_declaration | None: + cur_decl = content_type + while hasattr(cur_decl, "declared_type") is True: + cur_decl = cur_decl.declared_type() + if hasattr(cur_decl, "name") is False: + continue + if cur_decl.name() == "IfcLengthMeasure": + return cur_decl + + if isinstance(cur_decl, ifcopenshell.ifcopenshell_wrapper.aggregation_type): + res = cur_decl.type_of_element() + cur_decl = res.declared_type() + if hasattr(cur_decl, "name") and cur_decl.name() == "IfcLengthMeasure": + return cur_decl + while hasattr(cur_decl, "declared_type") is True: + cur_decl = cur_decl.declared_type() + if hasattr(cur_decl, "name") is False: + continue + if cur_decl.name() == "IfcLengthMeasure": + return cur_decl + + return None + + +def convert_file_units(ifc_file: ifcopenshell.file, target_units: str) -> ifcopenshell.file: + """Converts all units in an IFC file to the specified target units. Returns a new file.""" + prefix = "MILLI" if target_units == "MILLIMETERS" else None + + file_patched = ifcopenshell.api.run("project.create_file", version=ifc_file.schema) + if ifc_file.schema == "IFC2X3": + user = file_patched.add(ifc_file.by_type("IfcProject")[0].OwnerHistory.OwningUser) + application = file_patched.add(ifc_file.by_type("IfcProject")[0].OwnerHistory.OwningApplication) + old_get_user = ifcopenshell.api.owner.settings.get_user + old_get_application = ifcopenshell.api.owner.settings.get_application + ifcopenshell.api.owner.settings.get_user = lambda ifc: user + ifcopenshell.api.owner.settings.get_application = lambda ifc: application + + # Copy all elements from the original file to the patched file + for el in ifc_file: + file_patched.add(el) + + unit_assignment = ifcopenshell.util.unit.get_unit_assignment(file_patched) + + old_length = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT"][0] + new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix) + + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + # Traverse all elements and their nested attributes in the file and convert them + for element in file_patched: + entity = schema.declaration_by_name(element.is_a()) + attrs = entity.all_attributes() + for i, (attr, val, is_derived) in enumerate(zip(attrs, list(element), entity.derived())): + if is_derived: + continue + # Get all methods and attributes of the element + attr_type = attr.type_of_attribute() + base_type = get_base_type_name(attr_type) + if base_type is None: + continue + if val is None: + continue + if isinstance(val, tuple): + new_el = [ifcopenshell.util.unit.convert_unit(v, old_length, new_length) for v in val] + setattr(element, attr.name(), tuple(new_el)) + else: + new_el = ifcopenshell.util.unit.convert_unit(val, old_length, new_length) + # set the new value + setattr(element, attr.name(), new_el) + + file_patched.remove(old_length) + + if ifc_file.schema == "IFC2X3": + ifcopenshell.api.owner.settings.get_user = old_get_user + ifcopenshell.api.owner.settings.get_application = old_get_application + + return file_patched diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py index b18f0f670e..4aa8ee45a4 100644 --- a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py +++ b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py @@ -21,6 +21,7 @@ import ifcopenshell.api import ifcopenshell.api.owner.settings import ifcopenshell.util.pset import ifcopenshell.util.element +import ifcopenshell.util.unit class Patcher: @@ -47,44 +48,7 @@ class Patcher: self.file = file self.logger = logger self.unit = unit + self.file_patched: ifcopenshell.file = None def patch(self): - unit = {"is_metric": "METERS" in self.unit, "raw": self.unit} - self.file_patched = ifcopenshell.api.run("project.create_file", version=self.file.schema) - if self.file.schema == "IFC2X3": - user = self.file_patched.add(self.file.by_type("IfcProject")[0].OwnerHistory.OwningUser) - application = self.file_patched.add(self.file.by_type("IfcProject")[0].OwnerHistory.OwningApplication) - old_get_user = ifcopenshell.api.owner.settings.get_user - old_get_application = ifcopenshell.api.owner.settings.get_application - ifcopenshell.api.owner.settings.get_user = lambda ifc: user - ifcopenshell.api.owner.settings.get_application = lambda ifc: application - project = ifcopenshell.api.run("root.create_entity", self.file_patched, ifc_class="IfcProject") - unit_assignment = ifcopenshell.api.run("unit.assign_unit", self.file_patched, **{"length": unit}) - - # Is there a better way? - for element in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - element.Precision = 1e-8 - - # If we don't add openings first, they don't get converted - for element in self.file.by_type("IfcOpeningElement"): - self.file_patched.add(element) - - for element in self.file: - self.file_patched.add(element) - - new_length = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT"][0] - old_length = [ - u - for u in self.file_patched.by_type("IfcProject")[1].UnitsInContext.Units - if getattr(u, "UnitType", None) == "LENGTHUNIT" - ][0] - - for inverse in self.file_patched.get_inverse(old_length): - ifcopenshell.util.element.replace_attribute(inverse, old_length, new_length) - - self.file_patched.remove(old_length) - self.file_patched.remove(project) - - if self.file.schema == "IFC2X3": - ifcopenshell.api.owner.settings.get_user = old_get_user - ifcopenshell.api.owner.settings.get_application = old_get_application + self.file_patched = ifcopenshell.util.unit.convert_file_units(self.file, self.unit)