diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index 6e56fc996b..fe08240d07 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -164,6 +164,7 @@ class BatchReassignClass: class Migrator: def __init__(self): self.migrated_ids = {} + self.attribute_overrides = {} self.class_4_to_2x3 = json.load(open(os.path.join(cwd, "class_4_to_2x3.json"), "r")) self.class_2x3_to_4 = json.load(open(os.path.join(cwd, "class_2x3_to_4.json"), "r")) @@ -224,7 +225,6 @@ class Migrator: } def preprocess(self, old_file: ifcopenshell.file, new_file: ifcopenshell.file): - self.attribute_overrides = {} to_delete = set() if old_file.schema == "IFC2X3" and new_file.schema == "IFC4": @@ -262,7 +262,7 @@ class Migrator: except: pass # print("Migrating", element) - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(new_file.schema) + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(new_file.schema_identifier) new_element = self.migrate_class(element, new_file) # print("Migrated class from {} to {}".format(element, new_element)) new_element_schema = schema.declaration_by_name(new_element.is_a()) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index ec3c2513d3..f729c047cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -417,16 +417,12 @@ def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcop """Get the default project unit of a particular unit type :param ifc_file: The IFC file. - :type ifc_file: ifcopenshell.file :param unit_type: The type of unit, taken from the list of IFC unit types, such as "LENGTHUNIT". - :type unit_type: str :return: The IFC unit entity, or nothing if there is no default project unit defined. - :rtype: Union[ifcopenshell.entity_instance, None] """ - unit_assignment = get_unit_assignment(ifc_file) - if unit_assignment: + if unit_assignment := get_unit_assignment(ifc_file): for unit in unit_assignment.Units or []: if getattr(unit, "UnitType", None) == unit_type: return unit @@ -591,13 +587,9 @@ def convert_unit(value: float, from_unit: ifcopenshell.entity_instance, to_unit: """Convert from one unit to another unit :param value: The numeric value you want to convert - :type value: float :param from_unit: The IfcNamedUnit to confirm from. - :type from_unit: ifcopenshell.entity_instance :param to_unit: The IfcNamedUnit to confirm from. - :type to_unit: ifcopenshell.entity_instance :return: The converted value. - :rtype: float """ return convert( value, getattr(from_unit, "Prefix", None), from_unit.Name, getattr(to_unit, "Prefix", None), to_unit.Name @@ -612,17 +604,11 @@ def convert(value: float, from_prefix: Optional[str], from_unit: str, to_prefix: already available as IFC entities, consider using convert_unit() instead. :param value: The numeric value you want to convert - :type value: float :param from_prefix: A prefix from IfcSIPrefix. Can be None - :type from_prefix: str, optional :param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name - :type from_unit: str :param to_prefix: A prefix from IfcSIPrefix. Can be None - :type to_prefix: str, optional :param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name - :type to_unit: str :return: The converted value. - :rtype: float """ if from_unit.lower() in si_conversions: value *= si_conversions[from_unit.lower()] @@ -656,11 +642,8 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN si_meters / unit_scale = ifc_project_length :param ifc_file: The IFC file. - :type ifc_file: ifcopenshell.file :param unit_type: The type of SI unit, defaults to "LENGTHUNIT" - :type unit_type: str :returns: The scale factor - :rtype: float """ if not ifc_file.by_type("IfcUnitAssignment"): return 1 @@ -690,24 +673,19 @@ def format_length( :param value: The value in meters if metric, or either decimal feet or inches if imperial depending on input_unit. - :type value: float :param precision: How precise the format should be. I.e. round to nearest. For imperial, it is 1/Nth. E.g. 12 means to the nearest 1/12th of an inch. - :type precision: float :param decimal_places: How many decimal places to display. Defaults to 2. - :type decimal_places: int :param suppress_zero_inches: If imperial, whether or not to supress the inches if the inches is zero. - :type suppress_zero_inches: bool :param unit_system: Choose whether your value is "metric" or "imperial" - :type unit_system: str :param input_unit: If imperial, specify whether your value is "foot" or "inch". - :type input_unit: str :param output_unit: If imperial, specify whether your value is "foot" to format as both feet and inches, or "inch" if only inches should be shown. + :returns: The formatted string, such as 1' - 5 1/2". """ if unit_system == "imperial": if input_unit == "foot": @@ -747,9 +725,7 @@ def format_length( def is_attr_type( - content_type: ifcopenshell_wrapper.parameter_type, - ifc_unit_type_name: str, - include_select_types: bool = True + content_type: ifcopenshell_wrapper.parameter_type, ifc_unit_type_name: str, include_select_types: bool = True ) -> Union[ifcopenshell_wrapper.type_declaration, None]: cur_decl = content_type @@ -821,15 +797,16 @@ def iter_element_and_attributes_per_type( def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "METER") -> ifcopenshell.file: """Converts all units in an IFC file to the specified target units. Returns a new file.""" + import ifcopenshell.util.element + import ifcopenshell.util.geolocation + prefix = get_prefix(target_units) si_unit = get_unit_name(target_units) # Copy all elements from the original file to the patched file file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string()) - unit_assignment = get_unit_assignment(file_patched) - - old_length = next(u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT") + old_length = get_project_unit(file_patched, "LENGTHUNIT") if si_unit: new_length = ifcopenshell.api.unit.add_si_unit(file_patched, unit_type="LENGTHUNIT", prefix=prefix) else: @@ -856,7 +833,28 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " new_value = convert_value(val) setattr(element, attr.name(), new_value) + has_map_unit = False + if ( + ifc_file.schema == "IFC2X3" + and (crs := ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], name="ePSet_ProjectedCRS")) + and crs.get("MapUnit") + ) or (ifc_file.schema != "IFC2X3" and (crs := ifc_file.by_type("IfcProjectedCRS")) and crs[0].MapUnit): + has_map_unit = True + + if has_map_unit: + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(ifc_file) + ifcopenshell.api.georeference.edit_georeferencing( + file_patched, + coordinate_operation={ + "Eastings": parameters.e, + "Northings": parameters.n, + "OrthogonalHeight": parameters.h, + "Scale": parameters.scale / convert_value(1), + }, + ) + file_patched.remove(old_length) + unit_assignment = get_unit_assignment(file_patched) unit_assignment.Units = tuple([new_length, *unit_assignment.Units]) return file_patched diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 29a09fea5c..229435da73 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -19,6 +19,8 @@ import test.bootstrap import ifcopenshell.api.unit import ifcopenshell.api.root +import ifcopenshell.api.georeference +import ifcopenshell.util.geolocation import ifcopenshell.util.unit as subject from math import pi @@ -92,3 +94,73 @@ class TestIsAttrType(test.bootstrap.IFC4): assert subject.is_attr_type(nominal_value, "IfcValue") assert subject.is_attr_type(nominal_value, "IfcLengthMeasure") assert not subject.is_attr_type(nominal_value, "IfcLengthMeasure", include_select_types=False) + + +class TestConvertFileLengthUnits(test.bootstrap.IFC4): + def test_run(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + + def test_converting_map_conversion_if_there_is_no_map_unit(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Eastings": 10000}) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + assert output.by_type("IfcMapConversion")[0].Eastings == 10 + + def test_preserving_enh_if_there_is_a_map_unit(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, projected_crs={"MapUnit": meter}, coordinate_operation={"Eastings": 10, "Scale": 0.001} + ) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + assert output.by_type("IfcMapConversion")[0].Eastings == 10 + assert output.by_type("IfcMapConversion")[0].Northings == 0 + assert output.by_type("IfcMapConversion")[0].Scale == 1 + assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" + + +class TestConvertFileLengthUnitsIFC2X3(test.bootstrap.IFC2X3): + def test_converting_map_conversion_if_there_is_no_map_unit(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Eastings": 10000}) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) + assert parameters.e == 10 + + def test_preserving_enh_if_there_is_a_map_unit(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, projected_crs={"MapUnit": subject.get_full_unit_name(meter)}, coordinate_operation={"Eastings": 10, "Scale": 0.001} + ) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) + assert parameters.e == 10 + assert parameters.n == 0 + assert parameters.scale == 1 + crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") + assert crs["MapUnit"] == "METRE"