From c87baa1a50816ba3312aa0c6bdc439fa788f81d1 Mon Sep 17 00:00:00 2001 From: johltn Date: Sat, 16 Jan 2021 14:46:24 +0100 Subject: [PATCH] Clean branch --- src/ifcpatch/__init__.py | 65 ++++++++++++++++ src/ifcpatch/recipes/ExtractElements.py | 58 ++++++++++++++ src/ifcpatch/recipes/MergeProject.py | 19 +++++ src/ifcpatch/recipes/Migrate.py | 18 +++++ .../recipes/OffsetObjectPlacements.py | 75 +++++++++++++++++++ .../recipes/OffsetStoreyElevations.py | 30 ++++++++ src/ifcpatch/recipes/Optimise.py | 66 ++++++++++++++++ .../recipes/RecycleNonRootedElements.py | 35 +++++++++ .../recipes/RemoveSiteRepresentation.py | 24 ++++++ .../recipes/ResetAbsoluteCoordinates.py | 69 +++++++++++++++++ .../recipes/ResetSpatialElementLocations.py | 31 ++++++++ src/ifcpatch/recipes/SetRefElevation.py | 24 ++++++ src/ifcpatch/recipes/SplitByBuildingStorey.py | 42 +++++++++++ src/ifcpatch/recipes/__init__.py | 0 14 files changed, 556 insertions(+) create mode 100644 src/ifcpatch/__init__.py create mode 100644 src/ifcpatch/recipes/ExtractElements.py create mode 100644 src/ifcpatch/recipes/MergeProject.py create mode 100644 src/ifcpatch/recipes/Migrate.py create mode 100644 src/ifcpatch/recipes/OffsetObjectPlacements.py create mode 100644 src/ifcpatch/recipes/OffsetStoreyElevations.py create mode 100644 src/ifcpatch/recipes/Optimise.py create mode 100644 src/ifcpatch/recipes/RecycleNonRootedElements.py create mode 100644 src/ifcpatch/recipes/RemoveSiteRepresentation.py create mode 100644 src/ifcpatch/recipes/ResetAbsoluteCoordinates.py create mode 100644 src/ifcpatch/recipes/ResetSpatialElementLocations.py create mode 100644 src/ifcpatch/recipes/SetRefElevation.py create mode 100644 src/ifcpatch/recipes/SplitByBuildingStorey.py create mode 100644 src/ifcpatch/recipes/__init__.py diff --git a/src/ifcpatch/__init__.py b/src/ifcpatch/__init__.py new file mode 100644 index 0000000000..00b88b97af --- /dev/null +++ b/src/ifcpatch/__init__.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifcpatch.py` + +import ifcopenshell +import logging +import argparse + +def execute(args, is_library=None): + logging.basicConfig(filename=args['log'], filemode='a', level=logging.DEBUG) + logger = logging.getLogger('IFCPatch') + print('# Loading IFC file ...') + ifc_file = ifcopenshell.open(args['input']) + print('# Loading patch recipe ...') + patcher = getattr(__import__('recipes.{}'.format(args['recipe'])), args['recipe']).Patcher( + args['input'], ifc_file, logger, args['arguments']) + print('# Patching ...') + patcher.patch() + ifc_file = patcher.file + if is_library is True: + return ifc_file + print('# Writing patched file ...') + if not args['output']: + args['output'] = args['input'] + if isinstance(ifc_file, str): + with open(args['output'], 'w') as text_file: + text_file.write(ifc_file) + else: + ifc_file.write(args['output']) + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Patches IFC files to fix badly formatted data') + parser.add_argument( + '-i', + '--input', + type=str, + required=True, + help='The IFC file to patch') + parser.add_argument( + '-o', + '--output', + type=str, + help='The output file to save the patched IFC') + parser.add_argument( + '-r', + '--recipe', + type=str, + required=True, + help='Name of the recipe to use when patching') + parser.add_argument( + '-l', + '--log', + type=str, + help='Specify a log file', + default='ifcpatch.log') + parser.add_argument( + '-a', + '--arguments', + nargs='+', + help='Specify custom arguments to the patch recipe') + args = vars(parser.parse_args()) + + execute(args) + + print('# All tasks are complete :-)') diff --git a/src/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/recipes/ExtractElements.py new file mode 100644 index 0000000000..8ceb3a353f --- /dev/null +++ b/src/ifcpatch/recipes/ExtractElements.py @@ -0,0 +1,58 @@ +import ifcopenshell +import ifcopenshell.util.selector + + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + self.contained_ins = {} + self.aggregates = {} + self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema) + self.owner_history = None + for owner_history in self.file.by_type("IfcOwnerHistory"): + self.owner_history = self.new.add(owner_history) + break + selector = ifcopenshell.util.selector.Selector() + for element in selector.parse(self.file, self.args[0]): + self.add_element(element) + self.create_spatial_tree() + self.file = self.new + + def add_element(self, element): + new_element = self.new.add(element) + for rel in element.ContainedInStructure: + spatial_element = rel.RelatingStructure + new_spatial_element = self.new.add(spatial_element) + self.contained_ins.setdefault(spatial_element.GlobalId, set()).add(new_element) + self.add_spatial_tree(spatial_element, new_spatial_element) + + def add_spatial_tree(self, spatial_element, new_spatial_element): + for rel in spatial_element.Decomposes: + new = self.new.add(rel.RelatingObject) + self.aggregates.setdefault(rel.RelatingObject.GlobalId, set()).add(new_spatial_element) + self.add_spatial_tree(rel.RelatingObject, new) + + def create_spatial_tree(self): + for relating_structure, related_elements in self.contained_ins.items(): + self.new.createIfcRelContainedInSpatialStructure( + ifcopenshell.guid.new(), + self.owner_history, + None, + None, + list(related_elements), + self.new.by_guid(relating_structure), + ) + for relating_object, related_objects in self.aggregates.items(): + self.new.createIfcRelAggregates( + ifcopenshell.guid.new(), + self.owner_history, + None, + None, + self.new.by_guid(relating_object), + list(related_objects), + ) diff --git a/src/ifcpatch/recipes/MergeProject.py b/src/ifcpatch/recipes/MergeProject.py new file mode 100644 index 0000000000..cfe775551f --- /dev/null +++ b/src/ifcpatch/recipes/MergeProject.py @@ -0,0 +1,19 @@ +import ifcopenshell +import ifcopenshell.util.element + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + source = ifcopenshell.open(self.args[0]) + original_project = self.file.by_type('IfcProject')[0] + merged_project = self.file.add(source.by_type('IfcProject')[0]) + for element in source.by_type('IfcRoot'): + self.file.add(element) + for inverse in self.file.get_inverse(merged_project): + ifcopenshell.util.element.replace_attribute(inverse, merged_project, original_project) + self.file.remove(merged_project) diff --git a/src/ifcpatch/recipes/Migrate.py b/src/ifcpatch/recipes/Migrate.py new file mode 100644 index 0000000000..610cb4934d --- /dev/null +++ b/src/ifcpatch/recipes/Migrate.py @@ -0,0 +1,18 @@ +import ifcopenshell +import ifcopenshell.util.schema + + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + self.new = ifcopenshell.file(schema=self.args[0]) + migrator = ifcopenshell.util.schema.Migrator() + for element in self.file: + print("Migrating", element) + print("Successfully converted to", migrator.migrate(element, self.new)) + self.file = self.new diff --git a/src/ifcpatch/recipes/OffsetObjectPlacements.py b/src/ifcpatch/recipes/OffsetObjectPlacements.py new file mode 100644 index 0000000000..d9da46c79a --- /dev/null +++ b/src/ifcpatch/recipes/OffsetObjectPlacements.py @@ -0,0 +1,75 @@ +import math + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + absolute_placements = [] + + for product in self.file.by_type('IfcProduct'): + if not product.ObjectPlacement: + continue + absolute_placement = self.get_absolute_placement(product.ObjectPlacement) + if absolute_placement.is_a('IfcLocalPlacement'): + absolute_placements.append(absolute_placement) + absolute_placements = set(absolute_placements) + + for placement in absolute_placements: + offset_location = ( + placement.RelativePlacement.Location.Coordinates[0] + float(self.args[0]), + placement.RelativePlacement.Location.Coordinates[1] + float(self.args[1]), + placement.RelativePlacement.Location.Coordinates[2] + float(self.args[2]) + ) + + relative_placement = self.file.createIfcAxis2Placement3D( + self.file.createIfcCartesianPoint(offset_location)) + + if placement.RelativePlacement.Axis: + relative_placement.Axis = placement.RelativePlacement.Axis + if placement.RelativePlacement.RefDirection: + relative_placement.RefDirection = placement.RelativePlacement.RefDirection + + angle = float(self.args[3]) + if not angle: + placement.RelativePlacement = relative_placement + continue + + rotation_matrix = self.z_rotation_matrix(math.radians(angle)) + relative_placement.Location.Coordinates = self.multiply_by_matrix(offset_location, rotation_matrix) + + if placement.RelativePlacement.Axis: + z_axis = placement.RelativePlacement.Axis.DirectionRatios + relative_placement.Axis = self.file.createIfcDirection( + self.multiply_by_matrix(z_axis, rotation_matrix)) + + if placement.RelativePlacement.RefDirection: + x_axis = placement.RelativePlacement.RefDirection.DirectionRatios + else: + x_axis = (1., 0., 0.) + relative_placement.RefDirection = self.file.createIfcDirection( + self.multiply_by_matrix(x_axis, rotation_matrix)) + + placement.RelativePlacement = relative_placement + + def get_absolute_placement(self, object_placement): + if object_placement.PlacementRelTo: + return self.get_absolute_placement(object_placement.PlacementRelTo) + return object_placement + + def z_rotation_matrix(self, angle): + return [ + [math.cos(angle), -math.sin(angle), 0.], + [math.sin(angle), math.cos(angle), 0.], + [0., 0., 1.] + ] + + def multiply_by_matrix(self, v, m): + return [ + v[0]*m[0][0] + v[1]*m[0][1] + v[2]*m[0][2], + v[0]*m[1][0] + v[1]*m[1][1] + v[2]*m[1][2], + v[0]*m[2][0] + v[1]*m[2][1] + v[2]*m[2][2] + ] diff --git a/src/ifcpatch/recipes/OffsetStoreyElevations.py b/src/ifcpatch/recipes/OffsetStoreyElevations.py new file mode 100644 index 0000000000..c0384693c3 --- /dev/null +++ b/src/ifcpatch/recipes/OffsetStoreyElevations.py @@ -0,0 +1,30 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + project = self.file.by_type('IfcProject')[0] + storeys = self.find_decomposed_ifc_class(project, 'IfcBuildingStorey') + for storey in storeys: + co = storey.ObjectPlacement.RelativePlacement.Location.Coordinates + storey.ObjectPlacement.RelativePlacement.Location.Coordinates = (co[0], co[1], co[2]+float(self.args[0])) + co = storey.ObjectPlacement.RelativePlacement.Location.Coordinates + # NOTE If the geometric data is provided (ObjectPlacement is + # specified), the Elevation value shall either not be included, or + # be equal to the local placement Z value. + storey.Elevation = co[2] + + def find_decomposed_ifc_class(self, element, ifc_class): + results = [] + rel_aggregates = element.IsDecomposedBy + if not rel_aggregates: + return results + for rel_aggregate in rel_aggregates: + for part in rel_aggregate.RelatedObjects: + if part.is_a(ifc_class): + results.append(part) + results.extend(self.find_decomposed_ifc_class(part, ifc_class)) + return results diff --git a/src/ifcpatch/recipes/Optimise.py b/src/ifcpatch/recipes/Optimise.py new file mode 100644 index 0000000000..f990c65516 --- /dev/null +++ b/src/ifcpatch/recipes/Optimise.py @@ -0,0 +1,66 @@ +import ifcopenshell +import ifcopenshell.util.element +from toposort import toposort_flatten as toposort + + + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + self.optimized_file = ifcopenshell.file(schema=self.file.schema) + + + def patch(self): + def generate_instances_and_references(): + """ + Generator which yields an entity id and + the set of all of its references contained in its attributes. + """ + for inst in self.file: + yield inst.id(), set(i.id() for i in self.file.traverse(inst)[1:] if i.id()) + + instance_mapping = {} + + + def map_value(v): + """ + Recursive function which replicates an entity instance, with + its attributes, mapping references to already registered + instances. Indeed, because of the toposort we know that + forward attribute value instances are mapped before the instances + that reference them. + """ + if isinstance(v, (list, tuple)): + # lists are recursively traversed + return type(v)(map(map_value, v)) + elif isinstance(v, ifcopenshell.entity_instance): + if v.id() == 0: + # express simple types are not part of the toposort and just copied + return self.optimized_file.create_entity(v.is_a(), v[0]) + + + return instance_mapping[v] + else: + # a plain python value can just be returned + return v + + info_to_id = {} + + for id in toposort(dict(generate_instances_and_references())): + inst = self.file[id] + info = inst.get_info(include_identifier=False, recursive=True, return_type=frozenset) + if info in info_to_id: + mapped = instance_mapping[inst] = instance_mapping[self.file[info_to_id[info]]] + + else: + info_to_id[info] = id + instance_mapping[inst] = self.optimized_file.create_entity( + inst.is_a(), + *map(map_value, inst) + ) + + self.file = self.optimized_file + \ No newline at end of file diff --git a/src/ifcpatch/recipes/RecycleNonRootedElements.py b/src/ifcpatch/recipes/RecycleNonRootedElements.py new file mode 100644 index 0000000000..48ec179dd5 --- /dev/null +++ b/src/ifcpatch/recipes/RecycleNonRootedElements.py @@ -0,0 +1,35 @@ +from collections import deque +import ifcopenshell.util.element + +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + deleted = [] + hashes = {} + for element in self.file: + if element.is_a('IfcRoot'): + continue + h = hash(tuple(element)) + if h in hashes: + for inverse in self.file.get_inverse(element): + ifcopenshell.util.element.replace_attribute(inverse, element, hashes[h]) + deleted.append(element.id()) + else: + hashes[h] = element + deleted.sort() + deleted_q = deque(deleted) + new = '' + for line in self.file.wrapped_data.to_string().split('\n'): + try: + if int(line.split('=')[0][1:]) != deleted_q[0]: + new += (line + '\n') + else: + deleted_q.popleft() + except: + new += (line + '\n') + self.file = new diff --git a/src/ifcpatch/recipes/RemoveSiteRepresentation.py b/src/ifcpatch/recipes/RemoveSiteRepresentation.py new file mode 100644 index 0000000000..347cddce17 --- /dev/null +++ b/src/ifcpatch/recipes/RemoveSiteRepresentation.py @@ -0,0 +1,24 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + project = self.file.by_type('IfcProject')[0] + sites = self.find_decomposed_ifc_class(project, 'IfcSite') + for site in sites: + site.Representation = None + + def find_decomposed_ifc_class(self, element, ifc_class): + results = [] + rel_aggregates = element.IsDecomposedBy + if not rel_aggregates: + return results + for rel_aggregate in rel_aggregates: + for part in rel_aggregate.RelatedObjects: + if part.is_a(ifc_class): + results.append(part) + results.extend(self.find_decomposed_ifc_class(part, ifc_class)) + return results diff --git a/src/ifcpatch/recipes/ResetAbsoluteCoordinates.py b/src/ifcpatch/recipes/ResetAbsoluteCoordinates.py new file mode 100644 index 0000000000..83edff10a6 --- /dev/null +++ b/src/ifcpatch/recipes/ResetAbsoluteCoordinates.py @@ -0,0 +1,69 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + placement_coord_ids = set() + for placement in self.file.by_type('IfcObjectPlacement'): + [placement_coord_ids.add(e.id()) for e in self.file.traverse(placement) if e.is_a('IfcCartesianPoint')] + + # Arbitrary threshold based on experience + self.threshold = 1000000 + if self.args and len(self.args) == 1: + self.threshold = float(self.args[0]) + elif self.args and len(self.args) == 4: + self.threshold = float(self.args[3]) + + # This method will not work all the time, but will catch most issues. It + # assumes that absolute coordinates are easily recognisable based on + # having a large absolute value above a threshold. This is not always + # the case, but is very fast to run, and works for most cases. + offset_point = None + if self.args and len(self.args) >= 3: + offset_point = (float(self.args[0]),float(self.args[1]),float(self.args[2])) + try: + point_lists = self.file.by_type('IfcCartesianPointList3D') + except: + # IFC2X3 does not have IfcCartesianPointList3D + point_lists = [] + for point_list in point_lists: + coord_list = [None] * len(point_list.CoordList) + for i, point in enumerate(point_list.CoordList): + if len(point) == 2 or not self.is_point_far_away(point): + coord_list[i] = point + continue + if not offset_point: + offset_point = (-point[0], -point[1], -point[2]) + self.logger.info(f'Resetting absolute coordinates by {point}') + point = ( + point[0] + offset_point[0], + point[1] + offset_point[1], + point[2] + offset_point[2] + ) + coord_list[i] = point + point_list.CoordList = coord_list + for point in self.file.by_type('IfcCartesianPoint'): + if len(point.Coordinates) == 2 or not self.is_point_far_away(point): + continue + if point.id() in placement_coord_ids: + continue + if not offset_point: + offset_point = (-point.Coordinates[0], -point.Coordinates[1], -point.Coordinates[2]) + self.logger.info(f'Resetting absolute coordinates by {point}') + point.Coordinates = ( + point.Coordinates[0] + offset_point[0], + point.Coordinates[1] + offset_point[1], + point.Coordinates[2] + offset_point[2] + ) + + def is_point_far_away(self, point): + if hasattr(point, 'Coordinates'): + return abs(point.Coordinates[0]) > self.threshold \ + or abs(point.Coordinates[1]) > self.threshold \ + or abs(point.Coordinates[2]) > self.threshold + return abs(point[0]) > self.threshold \ + or abs(point[1]) > self.threshold \ + or abs(point[2]) > self.threshold diff --git a/src/ifcpatch/recipes/ResetSpatialElementLocations.py b/src/ifcpatch/recipes/ResetSpatialElementLocations.py new file mode 100644 index 0000000000..26812aeefc --- /dev/null +++ b/src/ifcpatch/recipes/ResetSpatialElementLocations.py @@ -0,0 +1,31 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + project = self.file.by_type('IfcProject')[0] + spatial_elements = self.find_decomposed_ifc_class(project, self.args[0]) + for spatial_element in spatial_elements: + self.patch_placement_to_origin(spatial_element) + + def find_decomposed_ifc_class(self, element, ifc_class): + results = [] + rel_aggregates = element.IsDecomposedBy + if not rel_aggregates: + return results + for rel_aggregate in rel_aggregates: + for part in rel_aggregate.RelatedObjects: + if part.is_a(ifc_class): + results.append(part) + results.extend(self.find_decomposed_ifc_class(part, ifc_class)) + return results + + def patch_placement_to_origin(self, element): + element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0., 0., 0.) + if element.ObjectPlacement.RelativePlacement.Axis: + element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0., 0., 1.) + if element.ObjectPlacement.RelativePlacement.RefDirection: + element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1., 0., 0.) diff --git a/src/ifcpatch/recipes/SetRefElevation.py b/src/ifcpatch/recipes/SetRefElevation.py new file mode 100644 index 0000000000..f06e8e77f6 --- /dev/null +++ b/src/ifcpatch/recipes/SetRefElevation.py @@ -0,0 +1,24 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + project = self.file.by_type('IfcProject')[0] + sites = self.find_decomposed_ifc_class(project, 'IfcSite') + for site in sites: + site.RefElevation = float(self.args[0]) + + def find_decomposed_ifc_class(self, element, ifc_class): + results = [] + rel_aggregates = element.IsDecomposedBy + if not rel_aggregates: + return results + for rel_aggregate in rel_aggregates: + for part in rel_aggregate.RelatedObjects: + if part.is_a(ifc_class): + results.append(part) + results.extend(self.find_decomposed_ifc_class(part, ifc_class)) + return results diff --git a/src/ifcpatch/recipes/SplitByBuildingStorey.py b/src/ifcpatch/recipes/SplitByBuildingStorey.py new file mode 100644 index 0000000000..44a112ea5e --- /dev/null +++ b/src/ifcpatch/recipes/SplitByBuildingStorey.py @@ -0,0 +1,42 @@ +class Patcher: + def __init__(self, src, file, logger, args=None): + self.src = src + self.file = file + self.logger = logger + self.args = args + + def patch(self): + import ifcopenshell + from shutil import copyfile + storeys = self.file.by_type('IfcBuildingStorey') + for i, storey in enumerate(storeys): + dest = '{}-{}.ifc'.format(i, storey.Name) + copyfile(self.src, dest) + old_ifc = ifcopenshell.open(dest) + new_ifc = ifcopenshell.file(schema=self.file.schema) + if self.file.schema == 'IFC2X3': + elements = old_ifc.by_type('IfcProject') + old_ifc.by_type('IfcProduct') + else: + elements = old_ifc.by_type('IfcContext') + old_ifc.by_type('IfcProduct') + inverse_elements = [] + for element in elements: + if element.is_a('IfcElement') \ + and not self.is_in_storey(element, storey): + element.Representation = None + continue + if element.is_a('IfcElement'): + styled_rep_items = [i for i in old_ifc.traverse(element) if i.is_a('IfcRepresentationItem') and i.StyledByItem] + [new_ifc.add(i.StyledByItem[0]) for i in styled_rep_items] + new_ifc.add(element) + inverse_elements.extend(old_ifc.get_inverse(element)) + for inverse_element in inverse_elements: + new_ifc.add(inverse_element) + for element in new_ifc.by_type('IfcElement'): + if not self.is_in_storey(element, storey): + new_ifc.remove(element) + new_ifc.write(dest) + + def is_in_storey(self, element, storey): + return element.ContainedInStructure \ + and element.ContainedInStructure[0].RelatingStructure.is_a('IfcBuildingStorey') \ + and element.ContainedInStructure[0].RelatingStructure.GlobalId == storey.GlobalId diff --git a/src/ifcpatch/recipes/__init__.py b/src/ifcpatch/recipes/__init__.py new file mode 100644 index 0000000000..e69de29bb2