From 94e3627c8ccceeafe137680aab4826b8dd44c147 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 22 Aug 2023 15:43:31 +1000 Subject: [PATCH] IfcCSV now supports grouping and group operations like concat, sum, min, max, etc. --- .../blenderbim/bim/module/csv/operator.py | 9 ++- .../blenderbim/bim/module/csv/prop.py | 16 ++++- .../blenderbim/bim/module/csv/ui.py | 8 ++- src/ifccsv/ifccsv.py | 61 +++++++++++++++++++ .../ifcopenshell/util/selector.py | 1 + 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index a5e7a3fe9c..db1d6cee52 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -81,6 +81,7 @@ class ImportCsvAttributes(bpy.types.Operator): new.name = attribute["name"] new.header = attribute["header"] new.sort = attribute["sort"] + new.group = attribute["group"] return {"FINISHED"} def invoke(self, context, event): @@ -102,7 +103,9 @@ class ExportCsvAttributes(bpy.types.Operator): data = { "query": tool.Search.export_filter_query(props.filter_groups), - "attributes": [{"name": a.name, "header": a.header, "sort": a.sort} for a in props.csv_attributes], + "attributes": [ + {"name": a.name, "header": a.header, "sort": a.sort, "group": a.group} for a in props.csv_attributes + ], } with open(self.filepath, "w") as outfile: @@ -148,9 +151,12 @@ class ExportIfcCsv(bpy.types.Operator): headers = [a.header for a in props.csv_attributes] sort = [] + groups = [] for attribute in props.csv_attributes: if attribute.sort != "NONE": sort.append({"name": attribute.name, "order": attribute.sort}) + if attribute.group != "NONE": + groups.append({"name": attribute.name, "type": attribute.group, "varies_value": attribute.varies_value}) sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter ifc_csv.export( @@ -167,6 +173,7 @@ class ExportIfcCsv(bpy.types.Operator): bool_true=props.true_value, bool_false=props.false_value, sort=sort, + groups=groups, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index 123fef901d..86b32d2b15 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -36,6 +36,19 @@ 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", "")]) + group: EnumProperty( + items=[ + ("NONE", "None", ""), + ("GROUP", "Group", "All rows where this value is identical will be merged."), + ("CONCAT", "Concatenation", "Concatenate values if values vary within a group."), + ("VARIES", "Varies", "Show a custom value if values vary within a group."), + ("SUM", "Sum", "Sums the total value of rows in a group."), + ("AVERAGE", "Average", "Averages the total value of rows in a group."), + ("MIN", "Min", "Gets the minimum value of rows in a group."), + ("MAX", "Max", "Gets the maximum value of rows in a group."), + ] + ) + varies_value: StringProperty(default="Varies", name="Varies Value") class CsvProperties(PropertyGroup): @@ -81,5 +94,6 @@ 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_show_sort: BoolProperty(default=False, name="Show Sorting") + should_show_group: BoolProperty(default=False, name="Show Grouping") 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 31d4c5c4e4..fb548a727e 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -48,6 +48,7 @@ class BIM_PT_ifccsv(Panel): row.operator("bim.import_csv_attributes", icon="IMPORT", text="") row.operator("bim.export_csv_attributes", icon="EXPORT", text="") row.prop(props, "should_show_sort", icon="SORTSIZE", text="") + row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="") row.prop(props, "should_show_settings", icon="PREFERENCES", text="") if not IfcStore.get_file() or not props.should_load_from_memory: @@ -90,8 +91,11 @@ class BIM_PT_ifccsv(Panel): row.prop(attribute, "name", text="") if props.should_show_sort: row.prop(attribute, "sort", text="") - else: - row.prop(attribute, "header", text="") + if props.should_show_group: + row.prop(attribute, "group", text="") + if attribute.group == "VARIES": + row.prop(attribute, "varies_value", 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 792861ddb0..9849227b99 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -28,6 +28,7 @@ import ifcopenshell import ifcopenshell.util.selector import ifcopenshell.util.element import ifcopenshell.util.schema +from statistics import mean try: from odf.namespaces import OFFICENS @@ -72,6 +73,7 @@ class IfcCsv: bool_true="YES", bool_false="NO", sort=None, + groups=None, ): self.ifc_file = ifc_file self.results = [] @@ -110,6 +112,65 @@ class IfcCsv: else: self.headers.append(attribute) + if groups: + group_results = {} + group_indices = {} + group_values = {} + group_varies_values = {} + + for group in groups: + index = attributes.index(group["name"]) + group_indices.setdefault(group["type"], []) + group_indices[group["type"]].append(index) + if group["type"] == "VARIES": + group_varies_values[index] = group["varies_value"] + + for row in self.results: + key = "-".join([str(row[gi]) for gi in group_indices.get("GROUP", [])]) + for group_type, gis in group_indices.items(): + if group_type in ("CONCAT", "VARIES"): + for gi in gis: + group_values.setdefault(key, {}).setdefault(gi, set()) + group_values[key][gi].add(str(row[gi])) + elif group_type in ("SUM", "AVERAGE", "MIN", "MAX"): + for gi in gis: + group_values.setdefault(key, {}).setdefault(gi, []) + try: + value = float(row[gi]) + except: + continue + group_values[key][gi].append(value) + group_results[key] = row + + for group_type, gis in group_indices.items(): + if group_type == "CONCAT": + for key, result in group_results.items(): + for gi in gis: + result[gi] = ", ".join(group_values[key][gi]) + elif group_type == "VARIES": + for key, result in group_results.items(): + for gi in gis: + if len(group_values[key][gi]) > 1: + result[gi] = group_varies_values[gi] + elif group_type == "SUM": + for key, result in group_results.items(): + for gi in gis: + result[gi] = sum(group_values[key][gi]) + elif group_type == "AVERAGE": + for key, result in group_results.items(): + for gi in gis: + result[gi] = mean(group_values[key][gi]) + elif group_type == "MIN": + for key, result in group_results.items(): + for gi in gis: + result[gi] = min(group_values[key][gi]) + elif group_type == "MAX": + for key, result in group_results.items(): + for gi in gis: + result[gi] = max(group_values[key][gi]) + + self.results = group_results.values() + if sort: def natural_sort(value): if isinstance(value, str): diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index a4a2ce4f0f..1f62223b7b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -673,6 +673,7 @@ class Selector: value = len(list(value)) elif isinstance(value, (list, tuple)): value = len(value) + value = 1 elif key == "class": value = value.is_a() elif key == "id":