From 331b9cc44029d13cce23e3a918f28dc310f083a7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 19 Aug 2023 23:09:27 +1000 Subject: [PATCH] See #3587. IfcCSV now supports custom header titles. --- .../blenderbim/bim/module/csv/__init__.py | 1 + .../blenderbim/bim/module/csv/operator.py | 45 ++++++++----------- .../blenderbim/bim/module/csv/prop.py | 7 ++- .../blenderbim/bim/module/csv/ui.py | 1 + src/ifccsv/ifccsv.py | 36 +++++++++++---- .../ifcopenshell/util/selector.py | 18 ++++---- 6 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/__init__.py b/src/blenderbim/blenderbim/bim/module/csv/__init__.py index 0cc17cc86c..1f1502b695 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/csv/__init__.py @@ -28,6 +28,7 @@ classes = ( operator.SelectCsvIfcFile, operator.ImportCsvAttributes, operator.ExportCsvAttributes, + prop.CsvAttribute, prop.CsvProperties, ui.BIM_PT_ifccsv, ) diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index b027e26ce6..9e82522be4 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -71,19 +71,15 @@ class ImportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - csv_props = context.scene.CsvProperties - csv_json = json.load(open(self.filepath)) - - expression = csv_json.get("expression", "") - if expression: - csv_props.ifc_selector = expression - - attributes = csv_json.get("attributes", []) - if attributes: - csv_props.csv_attributes.clear() - for attribute in attributes: - csv_props.csv_attributes.add().name = attribute + props = context.scene.CsvProperties + data = json.load(open(self.filepath)) + tool.Search.import_filter_query(data["query"], props.filter_groups) + props.csv_attributes.clear() + for i, attribute in enumerate(data["attributes"]): + new = props.csv_attributes.add() + new.name = attribute + new.header = data["headers"][i] return {"FINISHED"} def invoke(self, context, event): @@ -101,22 +97,16 @@ class ExportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - csv_props = context.scene.CsvProperties + props = context.scene.CsvProperties - csv_template = {} - expression = csv_props.ifc_selector - if expression: - csv_template["expression"] = expression - - csv_attributes = [] - for attribute in csv_props.csv_attributes: - attribute_name = attribute.name - csv_attributes.append(attribute_name) - if csv_attributes: - csv_template["attributes"] = csv_attributes + data = { + "query": tool.Search.export_filter_query(props.filter_groups), + "attributes": [a.name for a in props.csv_attributes], + "headers": [a.header for a in props.csv_attributes], + } with open(self.filepath, "w") as outfile: - json.dump(csv_template, outfile) + json.dump(data, outfile) return {"FINISHED"} @@ -155,11 +145,13 @@ class ExportIfcCsv(bpy.types.Operator): ifc_csv = ifccsv.IfcCsv() attributes = [a.name for a in props.csv_attributes] + headers = [a.header for a in props.csv_attributes] sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter ifc_csv.export( ifc_file, results, attributes, + headers=headers, output=self.filepath, format=props.format, delimiter=sep, @@ -190,7 +182,8 @@ class ImportIfcCsv(bpy.types.Operator): ifc_file = ifcopenshell.open(props.csv_ifc_file) ifc_csv = ifccsv.IfcCsv() sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter - ifc_csv.Import(ifc_file, self.filepath, delimiter=sep, null=props.null_value) + attributes = [a.name for a in props.csv_attributes] + ifc_csv.Import(ifc_file, self.filepath, attributes=attributes, delimiter=sep, null=props.null_value) if not props.should_load_from_memory: ifc_file.write(props.csv_ifc_file) purge_module_data() diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 6535001a9f..aca72d6dd9 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -32,6 +32,11 @@ from bpy.props import ( ) +class CsvAttribute(PropertyGroup): + name: StringProperty(name="Query", default="class") + header: StringProperty(name="Header Value", default="IFC Class") + + class CsvProperties(PropertyGroup): csv_ifc_file: StringProperty(default="", name="IFC File") ifc_selector: StringProperty(default="", name="IFC Selector") @@ -48,7 +53,7 @@ class CsvProperties(PropertyGroup): ("instance", "GlobalId", "", "GRIP", 7), ], ) - csv_attributes: CollectionProperty(name="CSV Attributes", type=StrProperty) + csv_attributes: CollectionProperty(name="CSV Attributes", type=CsvAttribute) null_value: StringProperty(default="N/A", name="Null Value") csv_delimiter: EnumProperty( items=[ diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index 2760894288..3a32559c4d 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -78,6 +78,7 @@ class BIM_PT_ifccsv(Panel): for index, attribute in enumerate(props.csv_attributes): row = layout.row(align=True) row.prop(attribute, "name", text="") + row.prop(attribute, "header", text="") row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row = layout.row(align=True) diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 88b6aa0b68..a5c6e17599 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -54,9 +54,11 @@ class IfcCsv: self.results = [] self.dataframe = None - def export(self, ifc_file, elements, attributes, output=None, format=None, delimiter=",", null=None): + def export(self, ifc_file, elements, attributes, headers=None, output=None, format=None, delimiter=",", null=None): self.ifc_file = ifc_file self.results = [] + if not headers: + headers = [None] * len(attributes) for element in elements: result = [] if hasattr(element, "GlobalId"): @@ -77,7 +79,11 @@ class IfcCsv: self.results.append(result) self.headers = ["GlobalId"] - self.headers.extend(attributes or []) + for i, attribute in enumerate(attributes or []): + if headers[i]: + self.headers.append(headers[i]) + else: + self.headers.append(attribute) if format == "csv": self.export_csv(output, delimiter=delimiter) @@ -184,7 +190,7 @@ class IfcCsv: results.update([p.Name for p in element.Quantities]) return ["{}.{}".format(pset_qto_name, n) for n in results] - def Import(self, ifc_file, table, delimiter=",", null="-"): + def Import(self, ifc_file, table, attributes=None, delimiter=",", null="-"): # Currently only supports CSV. with open(table, newline="", encoding="utf-8") as f: reader = csv.reader(f, delimiter=delimiter) @@ -192,6 +198,12 @@ class IfcCsv: for row in reader: if not headers: headers = row + print("csv", headers) + if not attributes: + attributes = [None] * len(headers) + elif len(attributes) == len(headers) - 1: + attributes.insert(0, "") # The GlobalId column + print("hea", attributes) continue try: element = ifc_file.by_guid(row[0]) @@ -203,7 +215,8 @@ class IfcCsv: continue # Skip GlobalId if value == null: value = None - ifcopenshell.util.selector.set_element_value(ifc_file, element, headers[i], value) + key = attributes[i] or headers[i] + ifcopenshell.util.selector.set_element_value(ifc_file, element, key, value) if __name__ == "__main__": @@ -218,9 +231,15 @@ if __name__ == "__main__": parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as "IfcWall"') parser.add_argument( "-a", - "--arguments", + "--attributes", nargs="+", - help="Specify attributes that are part of the extract, using the IfcQuery syntax such as 'type', 'Name' or 'Pset_Foo.Bar'", + help="Specify attributes that are part of the extract, using the IfcQuery syntax such as 'class', 'Name' or 'Pset_Foo.Bar'", + ) + parser.add_argument( + "-h", + "--headers", + nargs="+", + help="Specify human readable headers that correlate to each attribute.", ) 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") @@ -233,7 +252,8 @@ if __name__ == "__main__": ifc_csv.export( ifc_file, results, - args.arguments or [], + args.attributes or [], + headers=args.headers or [], output=args.spreadsheet, format=args.format, delimiter=args.delimiter, @@ -242,5 +262,5 @@ if __name__ == "__main__": elif getattr(args, "import"): ifc_csv = IfcCsv() ifc_file = ifcopenshell.open(args.ifc) - ifc_csv.Import(ifc_file, args.spreadsheet, delimiter=args.delimiter, null=args.null) + ifc_csv.Import(ifc_file, args.spreadsheet, attributes=args.attributes or [], delimiter=args.delimiter, null=args.null) ifc_file.write(args.ifc) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 5cabcd6fe6..4662f3245a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -152,7 +152,8 @@ def set_element_value(ifc_file, element, query, value): elif key == "container": element = ifcopenshell.util.element.get_container(element) elif key == "class": - return ifcopenshell.util.schema.reassign_class(ifc_file, element, value) + if element.is_a() != value: + return ifcopenshell.util.schema.reassign_class(ifc_file, element, value) elif key == "id": return elif isinstance(element, ifcopenshell.entity_instance): @@ -160,7 +161,8 @@ def set_element_value(ifc_file, element, query, value): key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it. if hasattr(element, key): - return setattr(element, key, value) + if getattr(element, key) != value: + return setattr(element, key, value) else: # Try to extract pset if is_regex: @@ -183,18 +185,16 @@ def set_element_value(ifc_file, element, query, value): element = result elif isinstance(element, dict): # Such as from the result of a prior get_pset pset = ifc_file.by_id(element["id"]) - if value in "NULL": - value = None if is_regex: - for prop in element.keys(): + for prop, prop_value in element.items(): if re.match(key, prop): - if pset.is_a("IfcPropertySet"): + if pset.is_a("IfcPropertySet") and prop_value != value: ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={prop: value}) - elif pset.is_a("IfcElementQuantity"): + elif pset.is_a("IfcElementQuantity") and prop_value != float(value): ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={prop: float(value)}) - elif pset.is_a("IfcPropertySet"): + elif pset.is_a("IfcPropertySet") and element[key] != value: ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={key: value}) - elif pset.is_a("IfcElementQuantity"): + elif pset.is_a("IfcElementQuantity") and element[key] != float(value): ifcopenshell.api.run("pset.edit_qto", ifc_file, qto=pset, properties={key: float(value)}) return elif isinstance(element, (list, tuple)): # If we use regex