From 83566845dfd89489f9fd8637fcb73c552cc8e70d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 22 Aug 2023 14:32:26 +1000 Subject: [PATCH] New support for sorting IfcCSV outputs --- .../blenderbim/bim/module/csv/operator.py | 17 ++++-- .../blenderbim/bim/module/csv/prop.py | 2 + .../blenderbim/bim/module/csv/ui.py | 9 ++-- src/ifccsv/ifccsv.py | 53 +++++++++++++------ 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index 7392af7d77..a5e7a3fe9c 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -76,10 +76,11 @@ class ImportCsvAttributes(bpy.types.Operator): tool.Search.import_filter_query(data["query"], props.filter_groups) props.csv_attributes.clear() - for i, attribute in enumerate(data["attributes"]): + for attribute in data["attributes"]: new = props.csv_attributes.add() - new.name = attribute - new.header = data["headers"][i] + new.name = attribute["name"] + new.header = attribute["header"] + new.sort = attribute["sort"] return {"FINISHED"} def invoke(self, context, event): @@ -101,8 +102,7 @@ class ExportCsvAttributes(bpy.types.Operator): 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], + "attributes": [{"name": a.name, "header": a.header, "sort": a.sort} for a in props.csv_attributes], } with open(self.filepath, "w") as outfile: @@ -146,6 +146,12 @@ 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] + + sort = [] + for attribute in props.csv_attributes: + if attribute.sort != "NONE": + sort.append({"name": attribute.name, "order": attribute.sort}) + sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter ifc_csv.export( ifc_file, @@ -160,6 +166,7 @@ class ExportIfcCsv(bpy.types.Operator): null=props.null_value, bool_true=props.true_value, bool_false=props.false_value, + sort=sort, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 86dcbf40df..123fef901d 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -35,6 +35,7 @@ from bpy.props import ( class CsvAttribute(PropertyGroup): name: StringProperty(name="Query", default="class") header: StringProperty(name="Header Value", default="IFC Class") + sort: EnumProperty(items=[("NONE", "None", ""), ("ASC", "Ascending", ""), ("DESC", "Descending", "")]) class CsvProperties(PropertyGroup): @@ -80,4 +81,5 @@ class CsvProperties(PropertyGroup): ) csv_custom_delimiter: StringProperty(default="", name="Custom Delimiter") should_show_settings: BoolProperty(default=False, name="Show Settings") + should_show_sort: BoolProperty(default=False, name="Show Sort") should_load_from_memory: BoolProperty(default=False, name="Load from Memory") diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index 1b9cd9ac52..31d4c5c4e4 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -42,13 +42,13 @@ class BIM_PT_ifccsv(Panel): row.prop(props, "should_load_from_memory") row.operator("bim.import_csv_attributes", icon="IMPORT", text="") row.operator("bim.export_csv_attributes", icon="EXPORT", text="") - row.prop(props, "should_show_settings", icon="PREFERENCES", text="") else: row = layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.import_csv_attributes", icon="IMPORT", text="") row.operator("bim.export_csv_attributes", icon="EXPORT", text="") - row.prop(props, "should_show_settings", icon="PREFERENCES", text="") + row.prop(props, "should_show_sort", icon="SORTSIZE", text="") + row.prop(props, "should_show_settings", icon="PREFERENCES", text="") if not IfcStore.get_file() or not props.should_load_from_memory: row = layout.row(align=True) @@ -88,7 +88,10 @@ 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="") + if props.should_show_sort: + row.prop(attribute, "sort", text="") + else: + 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 e35b693ca1..792861ddb0 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -21,6 +21,7 @@ # This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico ifccsv.py` import os +import re import csv import argparse import ifcopenshell @@ -70,20 +71,23 @@ class IfcCsv: null="-", bool_true="YES", bool_false="NO", + sort=None, ): self.ifc_file = ifc_file self.results = [] + self.headers = [] + attributes = attributes or [] + if not headers: headers = [None] * len(attributes) + + if include_global_id: + attributes.insert(0, "GlobalId") + headers.insert(0, "GlobalId") + for element in elements: result = [] - if include_global_id: - if hasattr(element, "GlobalId"): - result.append(element.GlobalId) - else: - result.append(None) - - for index, attribute in enumerate(attributes or []): + for index, attribute in enumerate(attributes): if "*" in attribute: attributes.extend(self.get_wildcard_attributes(attribute)) del attributes[index] @@ -99,13 +103,29 @@ class IfcCsv: result.append(value) self.results.append(result) - self.headers = ["GlobalId"] if include_global_id else [] - for i, attribute in enumerate(attributes or []): + self.headers = [] + for i, attribute in enumerate(attributes): if headers[i]: self.headers.append(headers[i]) else: self.headers.append(attribute) + if sort: + def natural_sort(value): + if isinstance(value, str): + convert = lambda text: int(text) if text.isdigit() else text.lower() + return [convert(c) for c in re.split('([0-9]+)', value)] + return value + + # Sort least important keys first, then more important keys. + # https://stackoverflow.com/questions/11476371/sort-by-multiple-keys-using-different-orderings + for sort_data in reversed(sort): + i = attributes.index(sort_data["name"]) + reverse = sort_data["order"] == "DESC" + self.results = sorted(self.results, key=lambda x: natural_sort(x[i]), reverse=reverse) + else: + self.results = sorted(self.results, key=lambda x: x[1 if include_global_id else 0]) + if format == "csv": self.export_csv(output, delimiter=delimiter) elif format == "ods": @@ -319,18 +339,20 @@ if __name__ == "__main__": 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.", + "-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") + parser.add_argument("--sort", nargs="+", help="Specify one or more attributes to sort by.") + parser.add_argument("--order", nargs="+", help="Choose the sort order from ASC or DESC for each sorted attribute.") + parser.add_argument("--export", action="store_true", help="Export from IFC to the desired format.") + parser.add_argument("--import", action="store_true", help="Import from the autodetected format to IFC.") args = parser.parse_args() if args.export: ifc_file = ifcopenshell.open(args.ifc) results = ifcopenshell.util.selector.filter_elements(ifc_file, args.query) + sort = None + if args.sort and len(args.sort) == len(args.order): + sort = [{"name": s, "order": args.order[i]} for i, s in enumerate(args.sort)] ifc_csv = IfcCsv() ifc_csv.export( ifc_file, @@ -343,6 +365,7 @@ if __name__ == "__main__": null=args.null, bool_true=args.bool_true, bool_false=args.bool_false, + sort=sort, ) elif getattr(args, "import"): ifc_csv = IfcCsv()