From 709c530434e7ec56eca159c926d003d2835dbf82 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 3 Aug 2022 17:33:23 +1000 Subject: [PATCH] Fix bug where MergeDuplicateTypesByTag recipe would've left multiple IfcRelDefinesByType relationships which is no good --- .../api/classification/add_reference.py | 4 +- .../api/classification/remove_reference.py | 2 - .../test_add_classification_reference.py | 13 ++++ .../test/util/test_pset.py | 6 +- .../recipes/MergeDuplicateTypesByTag.py | 65 ++++++++++++++++--- 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index b6d3f6deba..bd73c4b39d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -42,7 +42,7 @@ class Usecase: def add_from_identification(self): reference = self.get_existing_reference(self.settings["identification"]) if reference: - self.add_to_existing_relationship() + self.add_to_existing_relationship(reference) else: reference = self.file.createIfcClassificationReference( Name=self.settings["name"], ReferencedSource=self.settings["classification"] @@ -63,7 +63,7 @@ class Usecase: reference = self.get_existing_reference(identification) if reference: - self.add_to_existing_relationship() + self.add_to_existing_relationship(reference) return reference migrator = ifcopenshell.util.schema.Migrator() diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 860ad34383..86f2995938 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . -import ifcopenshell.util.schema - class Usecase: def __init__(self, file, **settings): 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 50a4d9ec66..c357811e27 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 @@ -40,6 +40,19 @@ class TestAddReference(test.bootstrap.IFC4): assert references[0].Name == "Foobar" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + 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] + def test_adding_a_reference_from_a_library(self): library = ifcopenshell.file() classification = library.createIfcClassification(Name="Name") diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 61331d2724..9fefd61396 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -36,15 +36,15 @@ class TestPsetQto: def test_getting_applicables_for_a_specific_predefined_type(self): names = self.pset_qto.get_applicable_names("IfcAudioVisualAppliance") - assert len(names) == 17 + assert len(names) == 12 assert "Pset_AudioVisualApplianceTypeAmplifier" not in names names = self.pset_qto.get_applicable_names("IfcAudioVisualAppliance", predefined_type="AMPLIFIER") assert "Pset_AudioVisualApplianceTypeAmplifier" in names - assert len(names) == 18 + assert len(names) == 13 def test_getting_a_pset_of_a_type_where_the_type_class_is_not_explicitly_applicable(self): names = self.pset_qto.get_applicable_names("IfcWall") assert "Pset_WallCommon" in names names = self.pset_qto.get_applicable_names("IfcWallType") - assert len(names) == 9 + assert len(names) == 5 assert "Pset_WallCommon" in names diff --git a/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypesByTag.py b/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypesByTag.py index eec13aecc9..345ff5bbce 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypesByTag.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypesByTag.py @@ -1,6 +1,5 @@ - # IfcPatch - IFC patching utiliy -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2020-2022 Dion Moult # # This file is part of IfcPatch. # @@ -30,11 +29,59 @@ class Patcher: def patch(self): tags = {} - for element in self.file.by_type("IfcTypeObject"): - original_element = tags.get(element.Tag, None) - if original_element: - for inverse in self.file.get_inverse(element): - ifcopenshell.util.element.replace_attribute(inverse, element, original_element) - self.file.remove(element) + for element_type in self.file.by_type("IfcTypeObject"): + original_type = tags.get(element_type.Tag, None) + if original_type: + for element in ifcopenshell.util.element.get_types(element_type): + self.assign_type(element, original_type) + for inverse in self.file.get_inverse(element_type): + ifcopenshell.util.element.replace_attribute(inverse, element_type, original_type) + self.file.remove(element_type) else: - tags[element.Tag] = element + tags[element_type.Tag] = element_type + + def assign_type(self, related_object, relating_type): + # This is basically a portion of the type.assign_type API which only + # affects the IfcRelDefinesByType relationship. To be conservative, we + # don't use the API directly since that would do other things like + # map type representations or recalculate material set usages which is + # risky when we're patching an existing dataset. + if self.file.schema == "IFC2X3": + is_typed_by = None + is_defined_by = related_object.IsDefinedBy + for rel in is_defined_by: + if rel.is_a("IfcRelDefinesByType"): + is_typed_by = [rel] + break + types = relating_type.ObjectTypeOf + else: + is_typed_by = related_object.IsTypedBy + types = relating_type.Types + + if types and is_typed_by == types: + return + + if is_typed_by: + related_objects = list(is_typed_by[0].RelatedObjects) + related_objects.remove(related_object) + if related_objects: + is_typed_by[0].RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": is_typed_by[0]}) + else: + self.file.remove(is_typed_by[0]) + + if types: + related_objects = list(types[0].RelatedObjects) + related_objects.append(related_object) + types[0].RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": types[0]}) + else: + types = self.file.create_entity( + "IfcRelDefinesByType", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [related_object], + "RelatingType": relating_type, + } + )