From 3c48cfbc1911aea9bf65fb19faf28c0475357b9d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 22 Jun 2024 21:59:09 +1000 Subject: [PATCH] Add support for IfcRigidOperation and IfcMapConversionScaled in API --- .../api/georeference/add_georeferencing.py | 33 ++++- .../api/georeference/edit_georeferencing.py | 22 ++-- .../test/util/test_geolocation.py | 120 +++++++++++++++--- 3 files changed, 141 insertions(+), 34 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py index 1c4656f625..4700e31a4b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py @@ -21,7 +21,7 @@ import ifcopenshell.api.pset import ifcopenshell.util.element -def add_georeferencing(file: ifcopenshell.file) -> None: +def add_georeferencing(file: ifcopenshell.file, ifc_class: str = "IfcMapConversion") -> None: """Add empty georeferencing entities to a model By default, models are not georeferenced. Georeferencing requires two @@ -35,8 +35,8 @@ def add_georeferencing(file: ifcopenshell.file) -> None: georeferencing parameters. See ifcopenshell.api.georeference.edit_georeferencing. - :return: None - :rtype: None + :param ifc_class: A type of IfcCoordinateOperation. For IFC2X3, this has no + impact and only uses ePSet_MapConversion. Example: @@ -73,6 +73,27 @@ def add_georeferencing(file: ifcopenshell.file) -> None: if not source_crs: return projected_crs = file.create_entity("IfcProjectedCRS", Name="") - file.create_entity( - "IfcMapConversion", SourceCRS=source_crs, TargetCRS=projected_crs, Eastings=0, Northings=0, OrthogonalHeight=0 - ) + if ifc_class == "IfcMapConversion": + file.create_entity( + ifc_class, SourceCRS=source_crs, TargetCRS=projected_crs, Eastings=0, Northings=0, OrthogonalHeight=0 + ) + elif ifc_class == "IfcMapConversionScaled": + file.create_entity( + ifc_class, + SourceCRS=source_crs, + TargetCRS=projected_crs, + Eastings=0, + Northings=0, + OrthogonalHeight=0, + FactorX=1, + FactorY=1, + FactorZ=1, + ) + elif ifc_class == "IfcRigidOperation": + file.create_entity( + ifc_class, + SourceCRS=source_crs, + TargetCRS=projected_crs, + FirstCoordinate=file.createIfcLengthMeasure(0), + SecondCoordinate=file.createIfcLengthMeasure(0), + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index ec01435505..1b09ffa88d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -22,7 +22,7 @@ from typing import Optional, Any def edit_georeferencing( file: ifcopenshell.file, - map_conversion: Optional[dict[str, Any]] = None, + coordinate_operation: Optional[dict[str, Any]] = None, projected_crs: Optional[dict[str, Any]] = None, true_north: Optional[tuple[float, float]] = None, ) -> None: @@ -48,9 +48,9 @@ def edit_georeferencing( See ifcopenshell.util.geolocation for more utilities to convert to and from local and map coordinates to check your results. - :param map_conversion: The IfcMapConversion dictionary of attribute - names and values you want to edit. - :type map_conversion: dict, optional + :param coordinate_operation: The dictionary of attribute names and values + you want to edit. + :type coordinate_operation: dict, optional :param projected_crs: The IfcProjectedCRS dictionary of attribute names and values you want to edit. :type projected_crs: dict, optional @@ -78,7 +78,7 @@ def edit_georeferencing( # Ordinate. ifcopenshell.api.run("georeference.edit_georeferencing", model, projected_crs={"Name": "EPSG:7856"}, - map_conversion={ + coordinate_operation={ "Eastings": 335087.17, # The architect nominates a false origin "Northings": 6251635.41, # The architect nominates a false origin # Note: this is the angle difference between Project North @@ -91,7 +91,7 @@ def edit_georeferencing( usecase = Usecase() usecase.file = file usecase.settings = { - "map_conversion": map_conversion or {}, + "coordinate_operation": coordinate_operation or {}, "projected_crs": projected_crs or {}, "true_north": true_north or [], } @@ -117,17 +117,17 @@ class Usecase: ifcopenshell.api.pset.edit_pset(self.file, crs, properties=self.settings["projected_crs"]) if (conversion := ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")): conversion = self.file.by_id(conversion["id"]) - for k, v in self.settings["map_conversion"].items(): + for k, v in self.settings["coordinate_operation"].items(): if k in ("XAxisAbscissa", "XAxisOrdinate", "Scale"): v = self.file.createIfcReal(v) else: v = self.file.createIfcLengthMeasure(v) - ifcopenshell.api.pset.edit_pset(self.file, conversion, properties=self.settings["map_conversion"]) + ifcopenshell.api.pset.edit_pset(self.file, conversion, properties=self.settings["coordinate_operation"]) return - map_conversion = self.file.by_type("IfcMapConversion")[0] + coordinate_operation = self.file.by_type("IfcCoordinateOperation")[0] projected_crs = self.file.by_type("IfcProjectedCRS")[0] - for name, value in self.settings["map_conversion"].items(): - setattr(map_conversion, name, value) + for name, value in self.settings["coordinate_operation"].items(): + setattr(coordinate_operation, name, value) for name, value in self.settings["projected_crs"].items(): setattr(projected_crs, name, value) diff --git a/src/ifcopenshell-python/test/util/test_geolocation.py b/src/ifcopenshell-python/test/util/test_geolocation.py index 76b2b339c8..1b54973d7a 100644 --- a/src/ifcopenshell-python/test/util/test_geolocation.py +++ b/src/ifcopenshell-python/test/util/test_geolocation.py @@ -56,7 +56,7 @@ class TestZ2E(test.bootstrap.IFC4): assert np.isclose(subject.z2e(1000, 2, 0.001, 0.9), 2.9) -class TestAutoXYZ2ENH(test.bootstrap.IFC4): +class TestAutoXYZ2ENH(test.bootstrap.IFC4X3): def test_no_georeferencing(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") assert subject.auto_xyz2enh(self.file, 0, 0, 0) == (0, 0, 0) @@ -69,20 +69,63 @@ class TestAutoXYZ2ENH(test.bootstrap.IFC4): ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"Name": "EPSG:7856"}, - map_conversion={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, ) assert subject.auto_xyz2enh(self.file, 0, 0, 0) == (1, 2, 3) assert subject.auto_xyz2enh(self.file, 1, 3, 5) == (2, 5, 8) - ifcopenshell.api.georeference.edit_georeferencing(self.file, map_conversion={"Scale": 0.001}) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) assert subject.auto_xyz2enh(self.file, 1000, 1000, 0) == (2, 3, 3) assert subject.auto_xyz2enh(self.file, 1000, 1000, 0, should_return_in_map_units=False) == (2000, 3000, 3000) ifcopenshell.api.georeference.edit_georeferencing( - self.file, map_conversion={"XAxisAbscissa": 0, "XAxisOrdinate": 1} + self.file, coordinate_operation={"XAxisAbscissa": 0, "XAxisOrdinate": 1} ) assert np.allclose(subject.auto_xyz2enh(self.file, 1000, 1000, 0), (0, 3, 3)) + def test_map_conversion_scaled(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file, ifc_class="IfcMapConversionScaled") + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + projected_crs={"Name": "EPSG:7856"}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + ) + assert subject.auto_xyz2enh(self.file, 0, 0, 0) == (1, 2, 3) + assert subject.auto_xyz2enh(self.file, 1, 3, 5) == (2, 5, 8) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) + assert subject.auto_xyz2enh(self.file, 1000, 1000, 0) == (2, 3, 3) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, coordinate_operation={"FactorX": 0.9, "FactorY": 0.9} + ) + assert np.allclose(subject.auto_xyz2enh(self.file, 1000, 1000, 0), (1.9, 2.9, 3)) -class TestAutoENH2XYZ(test.bootstrap.IFC4): + def test_rigid_operation(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file, ifc_class="IfcRigidOperation") + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + projected_crs={"Name": "EPSG:4258"}, + coordinate_operation={ + "FirstCoordinate": self.file.createIfcPlaneAngleMeasure(1), + "SecondCoordinate": self.file.createIfcPlaneAngleMeasure(2), + "Height": 3, + }, + ) + assert subject.auto_xyz2enh(self.file, 0, 0, 0) == (1, 2, 3) + assert subject.auto_xyz2enh(self.file, 1, 3, 5) == (2, 5, 8) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + coordinate_operation={ + "FirstCoordinate": self.file.createIfcLengthMeasure(1), + "SecondCoordinate": self.file.createIfcLengthMeasure(2), + }, + ) + assert subject.auto_xyz2enh(self.file, 0, 0, 0) == (1, 2, 3) + assert subject.auto_xyz2enh(self.file, 1, 3, 5) == (2, 5, 8) + + +class TestAutoENH2XYZ(test.bootstrap.IFC4X3): def test_no_georeferencing(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") assert subject.auto_enh2xyz(self.file, 0, 0, 0) == (0, 0, 0) @@ -95,20 +138,63 @@ class TestAutoENH2XYZ(test.bootstrap.IFC4): ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"Name": "EPSG:7856"}, - map_conversion={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, ) assert subject.auto_enh2xyz(self.file, 1, 2, 3) == (0, 0, 0) assert subject.auto_enh2xyz(self.file, 2, 5, 8) == (1, 3, 5) - ifcopenshell.api.georeference.edit_georeferencing(self.file, map_conversion={"Scale": 0.001}) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) assert np.allclose(subject.auto_enh2xyz(self.file, 2, 3, 3), (1000, 1000, 0)) assert np.allclose( subject.auto_enh2xyz(self.file, 2000, 3000, 3000, is_specified_in_map_units=False), (1000, 1000, 0) ) ifcopenshell.api.georeference.edit_georeferencing( - self.file, map_conversion={"XAxisAbscissa": 0, "XAxisOrdinate": 1} + self.file, coordinate_operation={"XAxisAbscissa": 0, "XAxisOrdinate": 1} ) assert np.allclose(subject.auto_enh2xyz(self.file, 0, 3, 3), (1000, 1000, 0)) + def test_map_conversion_scaled(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file, ifc_class="IfcMapConversionScaled") + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + projected_crs={"Name": "EPSG:7856"}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + ) + assert subject.auto_enh2xyz(self.file, 1, 2, 3) == (0, 0, 0) + assert subject.auto_enh2xyz(self.file, 2, 5, 8) == (1, 3, 5) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) + assert subject.auto_enh2xyz(self.file, 2, 3, 3) == (1000, 1000, 0) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, coordinate_operation={"FactorX": 0.9, "FactorY": 0.9} + ) + assert np.allclose(subject.auto_enh2xyz(self.file, 1.9, 2.9, 3), (1000, 1000, 0)) + + def test_rigid_operation(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + ifcopenshell.api.context.add_context(self.file, "Model") + ifcopenshell.api.georeference.add_georeferencing(self.file, ifc_class="IfcRigidOperation") + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + projected_crs={"Name": "EPSG:4258"}, + coordinate_operation={ + "FirstCoordinate": self.file.createIfcPlaneAngleMeasure(1), + "SecondCoordinate": self.file.createIfcPlaneAngleMeasure(2), + "Height": 3, + }, + ) + assert subject.auto_enh2xyz(self.file, 1, 2, 3) == (0, 0, 0) + assert subject.auto_enh2xyz(self.file, 2, 5, 8) == (1, 3, 5) + ifcopenshell.api.georeference.edit_georeferencing( + self.file, + coordinate_operation={ + "FirstCoordinate": self.file.createIfcLengthMeasure(1), + "SecondCoordinate": self.file.createIfcLengthMeasure(2), + }, + ) + assert subject.auto_enh2xyz(self.file, 1, 2, 3) == (0, 0, 0) + assert subject.auto_enh2xyz(self.file, 2, 5, 8) == (1, 3, 5) + class TestAutoZ2E(test.bootstrap.IFC4): def test_no_georeferencing(self): @@ -123,15 +209,15 @@ class TestAutoZ2E(test.bootstrap.IFC4): ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"Name": "EPSG:7856"}, - map_conversion={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, ) assert subject.auto_z2e(self.file, 0) == 3 assert subject.auto_z2e(self.file, 5) == 8 - ifcopenshell.api.georeference.edit_georeferencing(self.file, map_conversion={"Scale": 0.001}) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) assert np.isclose(subject.auto_z2e(self.file, 0), 3) assert np.isclose(subject.auto_z2e(self.file, 0, should_return_in_map_units=False), 3000) ifcopenshell.api.georeference.edit_georeferencing( - self.file, map_conversion={"XAxisAbscissa": 0, "XAxisOrdinate": 1} + self.file, coordinate_operation={"XAxisAbscissa": 0, "XAxisOrdinate": 1} ) assert np.isclose(subject.auto_z2e(self.file, 0), 3) @@ -221,16 +307,16 @@ class TestAutoLocal2Global(test.bootstrap.IFC4): ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"Name": "EPSG:7856"}, - map_conversion={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, ) m2[:, 3][0:3] = [1, 2, 3] assert np.allclose(subject.auto_local2global(self.file, m), m2) - ifcopenshell.api.georeference.edit_georeferencing(self.file, map_conversion={"Scale": 0.001}) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) assert np.allclose(subject.auto_local2global(self.file, m), m2) m2[:, 3][0:3] = [1000, 2000, 3000] assert np.allclose(subject.auto_local2global(self.file, m, should_return_in_map_units=False), m2) ifcopenshell.api.georeference.edit_georeferencing( - self.file, map_conversion={"XAxisAbscissa": 0, "XAxisOrdinate": 1} + self.file, coordinate_operation={"XAxisAbscissa": 0, "XAxisOrdinate": 1} ) m[:, 3][0:3] = [1000, 1000, 0] m2[:, 0][0:3] = [0, 1, 0] @@ -256,16 +342,16 @@ class TestAutoGlobal2Local(test.bootstrap.IFC4): ifcopenshell.api.georeference.edit_georeferencing( self.file, projected_crs={"Name": "EPSG:7856"}, - map_conversion={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, + coordinate_operation={"Eastings": 1, "Northings": 2, "OrthogonalHeight": 3}, ) m2[:, 3][0:3] = [1, 2, 3] assert np.allclose(subject.auto_global2local(self.file, m2), m) - ifcopenshell.api.georeference.edit_georeferencing(self.file, map_conversion={"Scale": 0.001}) + ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Scale": 0.001}) assert np.allclose(subject.auto_global2local(self.file, m2), m) m2[:, 3][0:3] = [1000, 2000, 3000] assert np.allclose(subject.auto_global2local(self.file, m2, is_specified_in_map_units=False), m) ifcopenshell.api.georeference.edit_georeferencing( - self.file, map_conversion={"XAxisAbscissa": 0, "XAxisOrdinate": 1} + self.file, coordinate_operation={"XAxisAbscissa": 0, "XAxisOrdinate": 1} ) m[:, 3][0:3] = [1000, 1000, 0] m2[:, 0][0:3] = [0, 1, 0]