Clean branch

This commit is contained in:
johltn
2021-01-16 14:46:24 +01:00
parent 30fa59960d
commit c87baa1a50
14 changed files with 556 additions and 0 deletions
+65
View File
@@ -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 :-)')
+58
View File
@@ -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),
)
+19
View File
@@ -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)
+18
View File
@@ -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
@@ -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]
]
@@ -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
+66
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.)
+24
View File
@@ -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
@@ -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
View File