From a5b5b10d2982bd84ca2ffe7005aec95fcb275895 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Mar 2020 13:19:35 +1100 Subject: [PATCH] Add IFCCSV tool - import and export data from CSV --- src/ifccsv/ifccsv.py | 295 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/ifccsv/ifccsv.py diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py new file mode 100644 index 0000000000..06175492bc --- /dev/null +++ b/src/ifccsv/ifccsv.py @@ -0,0 +1,295 @@ +# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifccsv.py` + +import ifcopenshell +import csv +import lark +import argparse + +class IfcSelectorParser(): + def __init__(self): + self.ifc = '' + self.file = None + self.query = '' + self.results = [] + + def parse(self): + self.file = ifcopenshell.open(self.ifc) + l = lark.Lark('''start: query (lfunction query)* + query: selector | group + group: "(" query (lfunction query)* ")" + selector: guid_selector | class_selector + guid_selector: "#" /[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$]{22}/ + class_selector: "." WORD filter ? + filter: "[" filter_key (comparison filter_value)? "]" + filter_key: WORD | pset_or_qto + filter_value: ESCAPED_STRING + pset_or_qto: /[A-Za-z0-9_]+/ "." /[A-Za-z0-9_]+/ + lfunction: and | or + and: "&" + or: "|" + comparison: contains | morethanequalto | lessthanequalto | equal | morethan | lessthan + contains: "*=" + morethanequalto: ">=" + lessthanequalto: "<" + equal: "=" + morethan: ">" + lessthan: "<" + + %import common.WORD // imports from terminal library + %import common.ESCAPED_STRING + %import common.WS + %ignore WS // Disregard spaces in text + ''') + + start = l.parse(self.query) + self.results = self.get_group(start) + + def get_group(self, group): + lfunction = None + for child in group.children: + if child.data == 'query': + new_results = self.get_query(child) + if not lfunction: + results = new_results + elif lfunction == 'or': + results.extend(new_results) + elif lfunction == 'and': + results = list(set(results).intersection(new_results)) + results = list(set(results)) + elif child.data == 'lfunction': + lfunction = child.children[0].data + return results + + def get_query(self, query): + for child in query.children: + if child.data == 'selector': + return self.get_selector(child) + elif child.data == 'group': + return self.get_group(child) + + def get_selector(self, selector): + for child in selector.children: + if child.data == 'class_selector': + return self.get_class_selector(child) + elif child.data == 'guid_selector': + return self.get_guid_selector(child) + + def get_class_selector(self, class_selector): + elements = self.file.by_type(class_selector.children[0]) + if len(class_selector.children) > 1 \ + and class_selector.children[1].data == 'filter': + return self.filter_elements(elements, class_selector.children[1]) + return elements + + def filter_elements(self, elements, filter_rule): + results = [] + key = filter_rule.children[0].children[0] + if not isinstance(key, str): + key = key.children[0] + '.' + key.children[1] + comparison = value = None + if len(filter_rule.children) > 1: + comparison = filter_rule.children[1].children[0].data + value = filter_rule.children[2].children[0][1:-1] + for element in elements: + element_value = IfcAttributeExtractor.get_element_key(element, key) + if not element_value: + continue + if not comparison \ + or self.filter_element(element, element_value, comparison, value): + results.append(element) + return results + + def filter_element(self, element, element_value, comparison, value): + if comparison == 'equal': + return str(element_value) == value + elif comparison == 'contains': + return value in str(element_value) + elif comparison == 'morethan': + return element_value > float(value) + elif comparison == 'lessthan': + return element_value < float(value) + elif comparison == 'morethanequalto': + return element_value >= float(value) + elif comparison == 'lessthanequalto': + return element_value <= float(value) + return False + + def get_guid_selector(self, guid_selector): + return [self.file.by_id(guid_selector.children[0])] + + +class IfcAttributeExtractor(): + @staticmethod + def get_element_key(element, key): + if '.' not in key \ + and hasattr(element, key): + return getattr(element, key) + elif key[0:3] == 'Qto': + qto_name, prop = key.split('.') + qto = IfcAttributeExtractor.get_element_qto(element, qto_name) + if qto: + return IfcAttributeExtractor.get_qto_property(qto, prop) + else: + pset_name, prop = key.split('.') + pset = IfcAttributeExtractor.get_element_pset(element, pset_name) + if pset: + return IfcAttributeExtractor.get_pset_property(pset, prop) + return None + + @staticmethod + def set_element_key(element, key, value): + if '.' not in key \ + and hasattr(element, key): + setattr(element, key, value) + elif key[0:3] == 'Qto': + qto, prop = key.split('.') + qto = IfcAttributeExtractor.get_element_qto(element, qto_name) + if qto: + IfcAttributeExtractor.set_qto_property(qto, prop, value) + else: + pset_name, prop = key.split('.') + pset = IfcAttributeExtractor.get_element_pset(element, pset_name) + if pset: + return IfcAttributeExtractor.set_pset_property(pset, prop, value) + + @staticmethod + def get_element_qto(element, name): + for relationship in element.IsDefinedBy: + if relationship.is_a('IfcRelDefinesByProperties') \ + and relationship.RelatingPropertyDefinition.is_a('IfcElementQuantity') \ + and relationship.RelatingPropertyDefinition.Name == name: + return relationship.RelatingPropertyDefinition + + @staticmethod + def get_qto_property(qto, name): + for prop in qto.Quantities: + if prop.Name != name: + continue + return getattr(prop, prop.is_a()[len('IfcQuantity'):] + 'Value') + + @staticmethod + def set_qto_property(qto, name, avlue): + for prop in qto.Quantities: + if prop.Name != name: + continue + setattr(prop, prop.is_a()[len('IfcQuantity'):] + 'Value', value) + + @staticmethod + def get_element_pset(element, name): + if element.is_a('IfcTypeObject'): + if element.HasPropertySets: + for pset in element.HasPropertySets: + if pset.is_a('IfcPropertySet') \ + and pset.Name == name: + return pset + else: + for relationship in element.IsDefinedBy: + if relationship.is_a('IfcRelDefinesByProperties') \ + and relationship.RelatingPropertyDefinition.is_a('IfcPropertySet') \ + and relationship.RelatingPropertyDefinition.Name == name: + return relationship.RelatingPropertyDefinition + + @staticmethod + def get_pset_property(pset, name): + for property in pset.HasProperties: + if property.Name == name: + return property.NominalValue.wrappedValue + + @staticmethod + def set_pset_property(pset, name, value): + for property in pset.HasProperties: + if property.Name == name: + property.NominalValue.wrappedValue = value + + +class IfcCsv(): + def __init__(self): + self.results = [] + self.attributes = [] + self.output = '' + + def export(self, elements): + for element in elements: + result = [] + if hasattr(element, 'GlobalId'): + result.append(element.GlobalId) + else: + result.append(None) + for attribute in self.attributes: + result.append(IfcAttributeExtractor.get_element_key(element, attribute)) + self.results.append(result) + + with open(self.output, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + header = ['GlobalId'] + header.extend(self.attributes) + writer.writerow(header) + for row in self.results: + writer.writerow(row) + + def Import(self, ifc): + ifc_file = ifcopenshell.open(ifc) + with open(self.output, newline='', encoding='utf-8') as f: + reader = csv.reader(f) + headers = [] + for row in reader: + if not headers: + headers = row + continue + element = ifc_file.by_guid(row[0]) + if not element: + continue + for i, value in enumerate(row): + if i == 0: + continue # Skip GlobalId + IfcAttributeExtractor.set_element_key(element, headers[i], value) + ifc_file.write(ifc) + +parser = argparse.ArgumentParser( + description='Exports IFC data to and from CSV') +parser.add_argument( + '-i', + '--ifc', + type=str, + required=True, + help='The IFC file') +parser.add_argument( + '-c', + '--csv', + type=str, + default='data.csv', + help='The CSV file to import from or export to') +parser.add_argument( + '-q', + '--query', + type=str, + default='', + help='Specify a IFC query selector, such as ".IfcWall"') +parser.add_argument( + '-a', + '--arguments', + nargs='+', + help='Specify attributes that are part of the extract') +parser.add_argument( + '--export', + action='store_true', + help='Export from IFC to CSV') +parser.add_argument( + '--import', + action='store_true', + help='Import from CSV to IFC') +args = parser.parse_args() + +if args.export: + ifc_selector_parser = IfcSelectorParser() + ifc_selector_parser.ifc = args.ifc + ifc_selector_parser.query = args.query + ifc_selector_parser.parse() + ifc_csv = IfcCsv() + ifc_csv.output = args.csv + ifc_csv.attributes = args.arguments if args.arguments else [] + ifc_csv.export(ifc_selector_parser.results) +elif getattr(args, 'import'): + ifc_csv = IfcCsv() + ifc_csv.output = args.csv + ifc_csv.Import(args.ifc)