Converting project length units now intelligently preserves map unit and coordinate operation scale.

This commit is contained in:
Dion Moult
2024-07-11 23:10:24 +10:00
parent a6940d2cba
commit e28b4d40ab
3 changed files with 102 additions and 32 deletions
@@ -164,6 +164,7 @@ class BatchReassignClass:
class Migrator: class Migrator:
def __init__(self): def __init__(self):
self.migrated_ids = {} 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_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")) 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): def preprocess(self, old_file: ifcopenshell.file, new_file: ifcopenshell.file):
self.attribute_overrides = {}
to_delete = set() to_delete = set()
if old_file.schema == "IFC2X3" and new_file.schema == "IFC4": if old_file.schema == "IFC2X3" and new_file.schema == "IFC4":
@@ -262,7 +262,7 @@ class Migrator:
except: except:
pass pass
# print("Migrating", element) # 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) new_element = self.migrate_class(element, new_file)
# print("Migrated class from {} to {}".format(element, new_element)) # print("Migrated class from {} to {}".format(element, new_element))
new_element_schema = schema.declaration_by_name(new_element.is_a()) new_element_schema = schema.declaration_by_name(new_element.is_a())
@@ -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 """Get the default project unit of a particular unit type
:param ifc_file: The IFC file. :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, :param unit_type: The type of unit, taken from the list of IFC unit types,
such as "LENGTHUNIT". such as "LENGTHUNIT".
:type unit_type: str
:return: The IFC unit entity, or nothing if there is no default project :return: The IFC unit entity, or nothing if there is no default project
unit defined. unit defined.
:rtype: Union[ifcopenshell.entity_instance, None]
""" """
unit_assignment = get_unit_assignment(ifc_file) if unit_assignment := get_unit_assignment(ifc_file):
if unit_assignment:
for unit in unit_assignment.Units or []: for unit in unit_assignment.Units or []:
if getattr(unit, "UnitType", None) == unit_type: if getattr(unit, "UnitType", None) == unit_type:
return unit 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 """Convert from one unit to another unit
:param value: The numeric value you want to convert :param value: The numeric value you want to convert
:type value: float
:param from_unit: The IfcNamedUnit to confirm from. :param from_unit: The IfcNamedUnit to confirm from.
:type from_unit: ifcopenshell.entity_instance
:param to_unit: The IfcNamedUnit to confirm from. :param to_unit: The IfcNamedUnit to confirm from.
:type to_unit: ifcopenshell.entity_instance
:return: The converted value. :return: The converted value.
:rtype: float
""" """
return convert( return convert(
value, getattr(from_unit, "Prefix", None), from_unit.Name, getattr(to_unit, "Prefix", None), to_unit.Name 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. already available as IFC entities, consider using convert_unit() instead.
:param value: The numeric value you want to convert :param value: The numeric value you want to convert
:type value: float
:param from_prefix: A prefix from IfcSIPrefix. Can be None :param from_prefix: A prefix from IfcSIPrefix. Can be None
:type from_prefix: str, optional
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name :param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type from_unit: str
:param to_prefix: A prefix from IfcSIPrefix. Can be None :param to_prefix: A prefix from IfcSIPrefix. Can be None
:type to_prefix: str, optional
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name :param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type to_unit: str
:return: The converted value. :return: The converted value.
:rtype: float
""" """
if from_unit.lower() in si_conversions: if from_unit.lower() in si_conversions:
value *= si_conversions[from_unit.lower()] 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 si_meters / unit_scale = ifc_project_length
:param ifc_file: The IFC file. :param ifc_file: The IFC file.
:type ifc_file: ifcopenshell.file
:param unit_type: The type of SI unit, defaults to "LENGTHUNIT" :param unit_type: The type of SI unit, defaults to "LENGTHUNIT"
:type unit_type: str
:returns: The scale factor :returns: The scale factor
:rtype: float
""" """
if not ifc_file.by_type("IfcUnitAssignment"): if not ifc_file.by_type("IfcUnitAssignment"):
return 1 return 1
@@ -690,24 +673,19 @@ def format_length(
:param value: The value in meters if metric, or either decimal feet or :param value: The value in meters if metric, or either decimal feet or
inches if imperial depending on input_unit. inches if imperial depending on input_unit.
:type value: float
:param precision: How precise the format should be. I.e. round to nearest. :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 For imperial, it is 1/Nth. E.g. 12 means to the nearest 1/12th of an
inch. inch.
:type precision: float
:param decimal_places: How many decimal places to display. Defaults to 2. :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 :param suppress_zero_inches: If imperial, whether or not to supress the
inches if the inches is zero. inches if the inches is zero.
:type suppress_zero_inches: bool
:param unit_system: Choose whether your value is "metric" or "imperial" :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 :param input_unit: If imperial, specify whether your value is "foot" or
"inch". "inch".
:type input_unit: str
:param output_unit: If imperial, specify whether your value is "foot" to :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 format as both feet and inches, or "inch" if only inches should be
shown. shown.
:returns: The formatted string, such as 1' - 5 1/2".
""" """
if unit_system == "imperial": if unit_system == "imperial":
if input_unit == "foot": if input_unit == "foot":
@@ -747,9 +725,7 @@ def format_length(
def is_attr_type( def is_attr_type(
content_type: ifcopenshell_wrapper.parameter_type, content_type: ifcopenshell_wrapper.parameter_type, ifc_unit_type_name: str, include_select_types: bool = True
ifc_unit_type_name: str,
include_select_types: bool = True
) -> Union[ifcopenshell_wrapper.type_declaration, None]: ) -> Union[ifcopenshell_wrapper.type_declaration, None]:
cur_decl = content_type 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: 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.""" """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) prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units) si_unit = get_unit_name(target_units)
# Copy all elements from the original file to the patched file # Copy all elements from the original file to the patched file
file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string()) file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string())
unit_assignment = get_unit_assignment(file_patched) old_length = get_project_unit(file_patched, "LENGTHUNIT")
old_length = next(u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT")
if si_unit: if si_unit:
new_length = ifcopenshell.api.unit.add_si_unit(file_patched, unit_type="LENGTHUNIT", prefix=prefix) new_length = ifcopenshell.api.unit.add_si_unit(file_patched, unit_type="LENGTHUNIT", prefix=prefix)
else: else:
@@ -856,7 +833,28 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
new_value = convert_value(val) new_value = convert_value(val)
setattr(element, attr.name(), new_value) 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) file_patched.remove(old_length)
unit_assignment = get_unit_assignment(file_patched)
unit_assignment.Units = tuple([new_length, *unit_assignment.Units]) unit_assignment.Units = tuple([new_length, *unit_assignment.Units])
return file_patched return file_patched
@@ -19,6 +19,8 @@
import test.bootstrap import test.bootstrap
import ifcopenshell.api.unit import ifcopenshell.api.unit
import ifcopenshell.api.root import ifcopenshell.api.root
import ifcopenshell.api.georeference
import ifcopenshell.util.geolocation
import ifcopenshell.util.unit as subject import ifcopenshell.util.unit as subject
from math import pi 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, "IfcValue")
assert subject.is_attr_type(nominal_value, "IfcLengthMeasure") assert subject.is_attr_type(nominal_value, "IfcLengthMeasure")
assert not subject.is_attr_type(nominal_value, "IfcLengthMeasure", include_select_types=False) 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"