diff --git a/src/ifcpatch/ifcpatch.py b/src/ifcpatch/ifcpatch.py new file mode 100644 index 0000000000..4268f2f534 --- /dev/null +++ b/src/ifcpatch/ifcpatch.py @@ -0,0 +1,54 @@ +import ifcopenshell +import logging +import argparse + +def execute(args): + 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__(f'recipes.{args.recipe}'), args.recipe).Patcher( + ifc_file, logger, args.arguments) + print('# Patching ...') + patcher.patch() + print('# Writing patched file ...') + if not args.output: + args.output = args.input + ifc_file.write(args.output) + +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 = parser.parse_args() + +execute(args) + +print('# All tasks are complete :-)') diff --git a/src/ifcpatch/recipes/OffsetStoreyElevations.py b/src/ifcpatch/recipes/OffsetStoreyElevations.py new file mode 100644 index 0000000000..356247942f --- /dev/null +++ b/src/ifcpatch/recipes/OffsetStoreyElevations.py @@ -0,0 +1,29 @@ +class Patcher: + def __init__(self, file, logger, args=None): + 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/RemoveSiteRepresentation.py b/src/ifcpatch/recipes/RemoveSiteRepresentation.py new file mode 100644 index 0000000000..b8a56dad3b --- /dev/null +++ b/src/ifcpatch/recipes/RemoveSiteRepresentation.py @@ -0,0 +1,23 @@ +class Patcher: + def __init__(self, file, logger, args=None): + 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..edb27ab2f9 --- /dev/null +++ b/src/ifcpatch/recipes/ResetAbsoluteCoordinates.py @@ -0,0 +1,26 @@ +class Patcher: + def __init__(self, file, logger): + self.file = file + self.logger = logger + + def patch(self): + # 12D can have some funky coordinates out of any sensible range. This + # method will not work all the time, but will catch most issues. + offset_point = None + for point in self.file.by_type('IfcCartesianPoint'): + if len(point.Coordinates) == 2 or not self.is_point_far_away(point): + 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): + # Arbitrary threshold based on experience + return abs(point.Coordinates[0]) > 1000000 \ + or abs(point.Coordinates[1]) > 1000000 \ + or abs(point.Coordinates[2]) > 1000000 diff --git a/src/ifcpatch/recipes/ResetSpatialElementLocations.py b/src/ifcpatch/recipes/ResetSpatialElementLocations.py new file mode 100644 index 0000000000..80b2689ed5 --- /dev/null +++ b/src/ifcpatch/recipes/ResetSpatialElementLocations.py @@ -0,0 +1,30 @@ +class Patcher: + def __init__(self, file, logger, args=None): + 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..edd687760a --- /dev/null +++ b/src/ifcpatch/recipes/SetRefElevation.py @@ -0,0 +1,23 @@ +class Patcher: + def __init__(self, file, logger, args=None): + 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/__init__.py b/src/ifcpatch/recipes/__init__.py new file mode 100644 index 0000000000..e69de29bb2