diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index bd73c4b39d..c8ebb9ab2b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -35,15 +35,14 @@ class Usecase: self.settings[key] = value def execute(self): + self.is_rooted = self.settings["product"].is_a("IfcRoot") if self.settings["reference"]: return self.add_from_library() return self.add_from_identification() def add_from_identification(self): reference = self.get_existing_reference(self.settings["identification"]) - if reference: - self.add_to_existing_relationship(reference) - else: + if not reference: reference = self.file.createIfcClassificationReference( Name=self.settings["name"], ReferencedSource=self.settings["classification"] ) @@ -51,6 +50,11 @@ class Usecase: reference.ItemReference = self.settings["identification"] else: reference.Identification = self.settings["identification"] + + relationship = self.get_existing_relationship(reference) + if relationship: + self.add_to_existing_relationship(relationship) + else: self.add_new_relationship(reference) return reference @@ -61,36 +65,39 @@ class Usecase: identification = self.settings["reference"].Identification reference = self.get_existing_reference(identification) + if not reference: + migrator = ifcopenshell.util.schema.Migrator() - if reference: + if self.settings["is_lightweight"]: + old_referenced_source = self.settings["reference"].ReferencedSource + self.settings["reference"].ReferencedSource = None + else: + existing_classification = [ + c for c in self.file.by_type("IfcClassification") if c.Name == self.settings["classification"].Name + ] + + reference = migrator.migrate(self.settings["reference"], self.file) + + if self.settings["is_lightweight"]: + reference.ReferencedSource = self.settings["classification"] + self.settings["reference"].ReferencedSource = old_referenced_source + elif existing_classification: + to_delete = set() + for traversed_reference in self.file.traverse(reference): + if traversed_reference.ReferencedSource.is_a("IfcClassification"): + to_delete.add(traversed_reference.ReferencedSource) + traversed_reference.ReferencedSource = existing_classification[0] + break + for element in to_delete: + self.file.remove(element) + + relationship = self.get_existing_relationship(reference) + if relationship: self.add_to_existing_relationship(reference) - return reference - - migrator = ifcopenshell.util.schema.Migrator() - - if self.settings["is_lightweight"]: - old_referenced_source = self.settings["reference"].ReferencedSource - self.settings["reference"].ReferencedSource = None else: - existing_classification = [ - c for c in self.file.by_type("IfcClassification") if c.Name == self.settings["classification"].Name - ] + self.add_new_relationship(reference) - reference = migrator.migrate(self.settings["reference"], self.file) - - if self.settings["is_lightweight"]: - reference.ReferencedSource = self.settings["classification"] - self.settings["reference"].ReferencedSource = old_referenced_source - elif existing_classification: - to_delete = set() - for traversed_reference in self.file.traverse(reference): - if traversed_reference.ReferencedSource.is_a("IfcClassification"): - to_delete.add(traversed_reference.ReferencedSource) - traversed_reference.ReferencedSource = existing_classification[0] - break - for element in to_delete: - self.file.remove(element) - self.add_new_relationship(reference) + return reference def get_existing_reference(self, identification): for reference in self.file.by_type("IfcClassificationReference"): @@ -102,23 +109,38 @@ class Usecase: return reference def add_new_relationship(self, reference): - self.file.create_entity( - "IfcRelAssociatesClassification", - GlobalId=ifcopenshell.guid.new(), - RelatedObjects=[self.settings["product"]], - RelatingClassification=reference, - ) + if self.is_rooted: + self.file.create_entity( + "IfcRelAssociatesClassification", + GlobalId=ifcopenshell.guid.new(), + RelatedObjects=[self.settings["product"]], + RelatingClassification=reference, + ) + else: + self.file.create_entity( + "IfcExternalReferenceRelationship", + RelatingReference=reference, + RelatedResourceObjects=[self.settings["product"]], + ) - def add_to_existing_relationship(self, reference): - rel = self.get_rel_associates_classification(reference) - related_objects = set(rel.RelatedObjects) - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) + def add_to_existing_relationship(self, rel): + if self.is_rooted: + related_objects = set(rel.RelatedObjects) + related_objects.add(self.settings["product"]) + rel.RelatedObjects = list(related_objects) + else: + related_objects = set(rel.RelatedResourceObjects) + related_objects.add(self.settings["product"]) + rel.RelatedResourceObjects = list(related_objects) - def get_rel_associates_classification(self, reference): - if self.file.schema == "IFC2X3": - for association in self.file.by_type("IfcRelAssociatesClassification"): - if association.RelatingClassification == reference: - return association - elif reference.ClassificationRefForObjects: - return reference.ClassificationRefForObjects[0] + def get_existing_relationship(self, reference): + if self.is_rooted: + if self.file.schema == "IFC2X3": + for rel in self.file.by_type("IfcRelAssociatesClassification"): + if rel.RelatingClassification == reference: + return rel + elif reference.ClassificationRefForObjects: + return reference.ClassificationRefForObjects[0] + elif self.file.schema != "IFC2X3": + if reference.ExternalReferenceForResources: + return reference.ExternalReferenceForResources[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 86f2995938..9d80660a12 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -25,20 +25,30 @@ class Usecase: self.settings[key] = value def execute(self): - total_related_objects = 0 - for association in self.file.by_type("IfcRelAssociatesClassification"): - if association.RelatingClassification == self.settings["reference"] and association.RelatedObjects: - total_related_objects += len(association.RelatedObjects) - related_objects = list(association.RelatedObjects) - try: - related_objects.remove(self.settings["product"]) - except: - continue - if len(related_objects): - association.RelatedObjects = related_objects - else: - self.file.remove(association) + if self.settings["product"].is_a("IfcRoot"): + for rel in self.file.by_type("IfcRelAssociatesClassification"): + if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects: + if self.settings["product"] in rel.RelatedObjects: + related_objects = list(rel.RelatedObjects) + related_objects.remove(self.settings["product"]) + if len(related_objects): + rel.RelatedObjects = related_objects + else: + self.file.remove(rel) + else: + for rel in self.file.by_type("IfcExternalReferenceRelationship"): + if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects: + if self.settings["product"] in rel.RelatedResourceObjects: + related_objects = list(rel.RelatedResourceObjects) + related_objects.remove(self.settings["product"]) + if len(related_objects): + rel.RelatedResourceObjects = related_objects + else: + self.file.remove(rel) # TODO: we only handle lightweight classifications here - if total_related_objects == 1: + if ( + not self.settings["reference"].ClassificationRefForObjects + and not self.settings["reference"].ExternalReferenceForResources + ): self.file.remove(self.settings["reference"]) diff --git a/src/ifcopenshell-python/ifcopenshell/util/classification.py b/src/ifcopenshell-python/ifcopenshell/util/classification.py index e0f60fd19c..65378f21a7 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/classification.py +++ b/src/ifcopenshell-python/ifcopenshell/util/classification.py @@ -21,6 +21,11 @@ import ifcopenshell.util.element def get_references(element, should_inherit=True): results = set() + if not element.is_a("IfcRoot"): + if hasattr(element, "HasExternalReferences"): + return {r.RelatingReference for r in element.HasExternalReferences or []} + elif hasattr(element, "HasExternalReference"): # Seriously, IFC? + return {r.RelatingReference for r in element.HasExternalReference or []} if should_inherit: element_type = ifcopenshell.util.element.get_type(element) if element_type and element_type != element: diff --git a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py index c357811e27..7cf5722b8f 100644 --- a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py @@ -53,7 +53,7 @@ class TestAddReference(test.bootstrap.IFC4): assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] - def test_adding_a_reference_from_a_library(self): + def test_adding_a_library_based_reference(self): library = ifcopenshell.file() classification = library.createIfcClassification(Name="Name") reference = library.createIfcClassificationReference(Identification="1", ReferencedSource=classification) @@ -72,3 +72,36 @@ class TestAddReference(test.bootstrap.IFC4): assert len(references) == 1 assert references[0].Identification == "1" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + + def test_adding_a_reference_to_a_resource(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = self.file.createIfcMaterial() + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + references = list(ifcopenshell.util.classification.get_references(element)) + assert len(references) == 1 + assert references[0].Identification == "X" + assert references[0].Name == "Foobar" + assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + + element2 = self.file.createIfcCostValue() + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element2, + identification="X", + name="Foobar", + classification=result, + ) + assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" + assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" + assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + + assert len(self.file.by_type("IfcExternalReferenceRelationship")[0].RelatedResourceObjects) == 2 diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py new file mode 100644 index 0000000000..597317f3bb --- /dev/null +++ b/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py @@ -0,0 +1,82 @@ +# 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 test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.classification + + +class TestRemoveReference(test.bootstrap.IFC4): + def test_removing_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(self.file.by_type("IfcClassificationReference")) == 0 + + def test_removing_a_reference_from_a_resource(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = self.file.createIfcMaterial() + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(self.file.by_type("IfcClassificationReference")) == 0 + + def test_retaining_the_reference_if_still_in_use(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = self.file.createIfcMaterial() + element2 = self.file.createIfcMaterial() + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + reference2 = ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element2, + identification="X", + name="Foobar", + classification=result, + ) + assert len(self.file.by_type("IfcClassificationReference")) == 1 + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(self.file.by_type("IfcClassificationReference")) == 1 + ifcopenshell.api.run("classification.remove_reference", self.file, product=element2, reference=reference2) + assert len(self.file.by_type("IfcClassificationReference")) == 0 diff --git a/src/ifcopenshell-python/test/util/test_classification.py b/src/ifcopenshell-python/test/util/test_classification.py index 7b838544e3..58a5e47de2 100644 --- a/src/ifcopenshell-python/test/util/test_classification.py +++ b/src/ifcopenshell-python/test/util/test_classification.py @@ -47,6 +47,20 @@ class TestGetReferences(test.bootstrap.IFC4): ) assert subject.get_references(element) == set(self.file.by_type("IfcClassificationReference")) + def test_get_references_of_a_non_rooted_element(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = self.file.createIfcMaterial() + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + assert subject.get_references(element) == set(self.file.by_type("IfcClassificationReference")) + def test_get_inherited_classifications(self): library = ifcopenshell.file() classification = library.createIfcClassification(Name="Name")