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:
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())
@@ -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
@@ -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"