From 520e7653e49d866b1fecf9f5106087d53951fbc6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 19 Aug 2022 18:43:39 +1000 Subject: [PATCH] #1375 #2317 add API support for referencing in spatial structures --- .../api/spatial/dereference_structure.py | 40 +++++++++++++ .../api/spatial/reference_structure.py | 57 +++++++++++++++++++ .../ifcopenshell/util/element.py | 55 ++++++++++++------ .../api/spatial/test_dereference_structure.py | 53 +++++++++++++++++ .../api/spatial/test_reference_structure.py | 50 ++++++++++++++++ .../test/util/test_element.py | 12 ++++ 6 files changed, 250 insertions(+), 17 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py create mode 100644 src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py create mode 100644 src/ifcopenshell-python/test/api/spatial/test_reference_structure.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py new file mode 100644 index 0000000000..d013cee5a6 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -0,0 +1,40 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"product": None, "relating_structure": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["product"].ReferencedInStructures: + if rel.RelatingStructure != self.settings["relating_structure"]: + continue + related_elements = list(rel.RelatedElements) + related_elements.remove(self.settings["product"]) + if related_elements: + rel.RelatedElements = related_elements + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + self.file.remove(rel) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py new file mode 100644 index 0000000000..32ca61e316 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -0,0 +1,57 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "product": None, + "relating_structure": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + referenced_in_structures = self.settings["product"].ReferencedInStructures + references_elements = self.settings["relating_structure"].ReferencesElements + + for rel in referenced_in_structures: + if rel.RelatingStructure == self.settings["relating_structure"]: + return + + if references_elements: + related_elements = list(references_elements[0].RelatedElements) + related_elements.append(self.settings["product"]) + references_elements[0].RelatedElements = related_elements + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": references_elements[0]}) + else: + references_elements = self.file.create_entity( + "IfcRelReferencedInSpatialStructure", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedElements": [self.settings["product"]], + "RelatingStructure": self.settings["relating_structure"], + } + ) + + return references_elements diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 03d7cadb3d..9264dae17d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -23,7 +23,7 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True): """Retrieve property sets, their related properties' names & values and ids. :param element: The IFC Element entity - :param psets_only: Default as False. Set to true if only property sets are needed. + :param psets_only: Default as False. Set to true if only property sets are needed. :param qtos_only: Default as False. Set to true if only quantities are needed. :param should_inherit: Default as True. Set to false if you don't want to inherit property sets from the Type. :return: dictionnary: key, value pair of psets' names and their properties' names & values @@ -133,7 +133,7 @@ def get_predefined_type(element): def get_type(element): """ Retrieves the Element Type entity related to an element entity. - + :param element: The IFC Element entity :return: The Element Type entity defining the element @@ -178,7 +178,7 @@ def get_material(element, should_skip_usage=False, should_inherit=True): def get_elements_by_material(ifc_file, material): """ Retrieves the elements related to a material. - + :param ifc_file: The IFC file :param material: The IFC Material entity :return: The elements related to the material @@ -212,7 +212,7 @@ def get_elements_by_material(ifc_file, material): def get_elements_by_style(ifc_file, style): """ Retrieves the elements related to a style. - + :param ifc_file: The IFC file :param style: The IFC Style entity :return: The elements related to the style @@ -282,17 +282,22 @@ def get_layers(ifc_file, element): def get_container(element, should_get_direct=False): """ - Retrieves the container of an element. - - :param element: The IFC element - :param should_get_direct: If True, the container of the element is returned. If False, the aggregate's container is returned - :return: The container of the element, or its aggregate's container. + Retrieves the spatial structure container of an element. + :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance + :param should_get_direct: If True, a result is only returned if the element + is directly contained in a spatial structure element. If False, an + indirect spatial container may be returned, such as if an element is a + part of an aggregate, and then if that aggregate is contained in a + spatial structure element. + :type should_get_direct: bool + :return: The direct or indirect container of the element or None. Example:: - element = file.by_type("IfcWall")[0] - container = ifcopenshell.util.element.get_container(element) + element = file.by_type("IfcWall")[0] + container = ifcopenshell.util.element.get_container(element) """ if should_get_direct: if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: @@ -305,17 +310,33 @@ def get_container(element, should_get_direct=False): return element.ContainedInStructure[0].RelatingStructure +def get_referenced_structures(element): + """ + Retreives a list of referenced structural elements + + :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance + + Example:: + + element = file.by_type("IfcWall")[0] + print(ifcopenshell.util.element.get_referenced_structures(element)) + """ + if hasattr(element, "ReferencedInStructures"): + return [r.RelatingStructure for r in element.ReferencedInStructures] + + def get_decomposition(element): """ Retrieves the decomposition of an element. - + :param element: The IFC element :return: The decomposition of the element Example:: - element = file.by_type("IfcProject")[0] - decomposition = ifcopenshell.util.element.get_decomposition(element) + element = file.by_type("IfcProject")[0] + decomposition = ifcopenshell.util.element.get_decomposition(element) """ queue = [element] results = [] @@ -333,7 +354,7 @@ def get_decomposition(element): def get_aggregate(element): """ Retrieves the aggregate of an element. - + :param element: The IFC element :return: The aggregate of the element @@ -349,14 +370,14 @@ def get_aggregate(element): def get_parts(element): """ Retrieves the parts of an element. - + :param element: The IFC element :return: The parts of the element Example:: element = file.by_type("IfcElementAssembly")[0] parts = ifcopenshell.util.element.get_parts(element) - + """ if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: return element.IsDecomposedBy[0].RelatedObjects diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py new file mode 100644 index 0000000000..1cf2f3108e --- /dev/null +++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py @@ -0,0 +1,53 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestDereferenceStructure(test.bootstrap.IFC4): + def test_removing_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + + def test_doing_nothing_if_no_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + + def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element) + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element) + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement1, relating_structure=element) + assert self.file.by_type("IfcRelReferencedInSpatialStructure")[0].RelatedElements == (subelement2,) + + def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 diff --git a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py new file mode 100644 index 0000000000..21b6b0e8e8 --- /dev/null +++ b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py @@ -0,0 +1,50 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestReferenceStructure(test.bootstrap.IFC4): + def test_referencing_a_structure(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + rel = ifcopenshell.api.run( + "spatial.reference_structure", self.file, product=subelement, relating_structure=element + ) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] + assert rel.is_a("IfcRelReferencedInSpatialStructure") + + def test_doing_nothing_if_the_structure_is_already_referenced(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + assert len([e for e in self.file]) == total_elements + + def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element1) + ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element1) + rel = subelement1.ReferencedInStructures[0] + assert len(rel.RelatedElements) == 2 diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index afae619e04..8eb80df8f0 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -538,6 +538,18 @@ class TestGetContainerIFC4(test.bootstrap.IFC4): assert subject.get_container(subelement, should_get_direct=True) is None +class TestGetReferencedStructures(test.bootstrap.IFC4): + def test_getting_references_of_an_element(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + assert subject.get_referenced_structures(element) == [] + building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building) + assert subject.get_referenced_structures(element) == [building] + building2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building2) + assert subject.get_referenced_structures(element) == [building, building2] + + class TestGetDecompositionIFC4(test.bootstrap.IFC4): def test_getting_decomposed_subelements_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly")