From 164e5c69ca438cf91447642ed78f5f7e06bdd171 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 18 Nov 2024 15:15:48 +0500 Subject: [PATCH] bim.export_resources #5760 Simple export for resources to csv file, supports just the same fields that import currently does. Location - https://i.imgur.com/Ssz7BwZ.png --- .../bonsai/bim/module/resource/__init__.py | 1 + .../bonsai/bim/module/resource/operator.py | 21 +++- src/bonsai/bonsai/bim/module/resource/ui.py | 1 + src/bonsai/bonsai/core/resource.py | 4 + src/bonsai/bonsai/tool/resource.py | 13 ++- src/ifc4d/ifc4d/csv2ifc.py | 110 ++++++++++++++++-- 6 files changed, 137 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/resource/__init__.py b/src/bonsai/bonsai/bim/module/resource/__init__.py index 002952b3c1..33b507e0a8 100644 --- a/src/bonsai/bonsai/bim/module/resource/__init__.py +++ b/src/bonsai/bonsai/bim/module/resource/__init__.py @@ -43,6 +43,7 @@ classes = ( operator.EnableEditingResourceBaseQuantity, operator.EnableEditingResourceCosts, operator.EnableEditingResourceCostValue, + operator.ExportResources, operator.CalculateResourceUsage, operator.EnableEditingResourceCostValueFormula, operator.EnableEditingResourceQuantity, diff --git a/src/bonsai/bonsai/bim/module/resource/operator.py b/src/bonsai/bonsai/bim/module/resource/operator.py index b09069f997..7a72ac0f32 100644 --- a/src/bonsai/bonsai/bim/module/resource/operator.py +++ b/src/bonsai/bonsai/bim/module/resource/operator.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . import bpy -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper from bonsai.bim.module.resource.ui import draw_productivity_ui import bonsai.core.resource as core import bonsai.tool as tool @@ -334,6 +334,7 @@ class EditResourceQuantity(bpy.types.Operator, tool.Ifc.Operator): class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_resources" bl_label = "Import Resources" + bl_description = "Import resources from a .csv file" bl_options = {"REGISTER", "UNDO"} filename_ext = ".csv" filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) @@ -351,6 +352,24 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): self.report({"INFO"}, f"{resources_after - resources_before} resources are imported.") +class ExportResources(bpy.types.Operator, tool.Ifc.Operator, ExportHelper): + bl_idname = "bim.export_resources" + bl_label = "Export Resources" + bl_description = "Export resources to a .csv file" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".csv" + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + @classmethod + def poll(cls, context): + ifc_file = tool.Ifc.get() + return ifc_file is not None + + def _execute(self, context): + core.export_resources(tool.Resource, file_path=self.filepath) + self.report({"INFO"}, f"Resources are exported to {self.filepath}") + + class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_productivity_data" bl_label = "Add Productivity" diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py index 5899e21275..41e20ae6f1 100644 --- a/src/bonsai/bonsai/bim/module/resource/ui.py +++ b/src/bonsai/bonsai/bim/module/resource/ui.py @@ -54,6 +54,7 @@ class BIM_PT_resources(Panel): else: row.operator("bim.load_resources", text="", icon="GREASEPENCIL") row.operator("bim.import_resources", text="", icon="IMPORT") + row.operator("bim.export_resources", text="", icon="EXPORT") if not self.props.is_editing: return diff --git a/src/bonsai/bonsai/core/resource.py b/src/bonsai/bonsai/core/resource.py index 12c6dc2b14..5a470094d0 100644 --- a/src/bonsai/bonsai/core/resource.py +++ b/src/bonsai/bonsai/core/resource.py @@ -171,6 +171,10 @@ def import_resources(resource_tool: tool.Resource, file_path: str) -> None: resource_tool.load_resources() +def export_resources(resource_tool: tool.Resource, file_path: str) -> None: + resource_tool.export_resources(file_path) + + def expand_resource(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None: resource_tool.expand_resource(resource) resource_tool.load_resources() diff --git a/src/bonsai/bonsai/tool/resource.py b/src/bonsai/bonsai/tool/resource.py index e00b71748e..45c64d3653 100644 --- a/src/bonsai/bonsai/tool/resource.py +++ b/src/bonsai/bonsai/tool/resource.py @@ -304,8 +304,17 @@ class Resource(bonsai.core.tool.Resource): from ifc4d.csv2ifc import Csv2Ifc start = time.time() - p62ifc = Csv2Ifc(file_path, tool.Ifc.get()) - p62ifc.execute() + csv2ifc = Csv2Ifc(file_path, tool.Ifc.get()) + csv2ifc.execute() + print("Importing Resources CSV finished in {:.2f} seconds".format(time.time() - start)) + + @classmethod + def export_resources(cls, file_path: str) -> None: + from ifc4d.csv2ifc import Ifc2Csv + + start = time.time() + csv2ifc = Ifc2Csv(file_path, tool.Ifc.get()) + csv2ifc.execute() print("Importing Resources CSV finished in {:.2f} seconds".format(time.time() - start)) @classmethod diff --git a/src/ifc4d/ifc4d/csv2ifc.py b/src/ifc4d/ifc4d/csv2ifc.py index da40fe980a..7c4034a051 100644 --- a/src/ifc4d/ifc4d/csv2ifc.py +++ b/src/ifc4d/ifc4d/csv2ifc.py @@ -16,13 +16,18 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcResources. If not, see . +from __future__ import annotations import csv +import _csv +import isodate import ifcopenshell import ifcopenshell.api import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.resource import ifcopenshell.util.unit import datetime -from typing import Any, Optional, Union, Literal, get_args +from typing import Any, Optional, Union, Literal, get_args, NamedTuple, cast SUPPORTED_COLUMN = Literal[ @@ -36,6 +41,15 @@ SUPPORTED_COLUMN = Literal[ "EQUIPMENT OUTPUT", ] +RESOURCE_MAP = { + "CREW": "IfcCrewResource", + "LABOR": "IfcLaborResource", + "EQUIPMENT": "IfcConstructionEquipmentResource", + "SUBCONTRACTOR": "IfcSubContractResource", + "MATERIAL": "IfcConstructionMaterialResource", + "PRODUCT": "IfcConstructionProductResource", +} + class Csv2Ifc: """Class to import resources from a CSV file into an IFC file. @@ -64,14 +78,6 @@ class Csv2Ifc: self.file = ifc_file self.resources: list[dict[str, Any]] = [] self.units = {} # TODO: never used - self.resource_map = { - "CREW": "IfcCrewResource", - "LABOR": "IfcLaborResource", - "EQUIPMENT": "IfcConstructionEquipmentResource", - "SUBCONTRACTOR": "IfcSubContractResource", - "MATERIAL": "IfcConstructionMaterialResource", - "PRODUCT": "IfcConstructionProductResource", - } def execute(self) -> None: self.parse_csv() @@ -114,7 +120,7 @@ class Csv2Ifc: def get_row_resource_data(self, row: list[str]) -> dict[str, Any]: name = row[self.headers["ACTIVITY/RESOURCE NAME"]] - resource_class = self.resource_map[row[self.headers["TYPE"]]] + resource_class = RESOURCE_MAP[row[self.headers["TYPE"]]] base_cost_value = row[self.headers["COST"]] productivity = {} @@ -194,3 +200,87 @@ class Csv2Ifc: self.file = ifcopenshell.file(schema="IFC4") ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") ifcopenshell.api.run("unit.assign_unit", self.file) + + +class Ifc2CsvRow(NamedTuple): + hierarchy: int + resource_type: str + name: str + description: str + cost: Union[float, None] + quantity_name: Union[str, None] + labor_output: Union[float, None] + equipment_output: Union[float, None] + + +class Ifc2Csv: + """Class to export resources from an IFC file to csv file.""" + + HEADER = ( + "HIERARCHY", + "TYPE", + "ACTIVITY/RESOURCE NAME", + "DESCRIPTION", + "COST", + "QUANTITY NAME", + "LABOR OUTPUT", + "EQUIPMENT OUTPUT", + "GUID", + ) + assert len(HEADER) == len(Ifc2CsvRow._fields) + + def __init__(self, filepath: str, ifc_file: ifcopenshell.file): + self.filepath = filepath + self.file = ifc_file + self.inverse_resource_map = {value: key for key, value in RESOURCE_MAP.items()} + + def execute(self) -> None: + # TODO: unused columns: USAGE, UNIT, PRODUCTIVITY UNIT. + with open(self.filepath, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(self.HEADER) + for resource in self.file.by_type("IfcConstructionResource"): + if resource.Nests: + continue + self.process_recourse(writer, resource, 1) + + def process_recourse( + self, writer: _csv._writer, resource: ifcopenshell.entity_instance, hierarchy_level: int + ) -> None: + row = self.get_row(resource, hierarchy_level) + writer.writerow(row) + resources = ifcopenshell.util.resource.get_nested_resources(resource) + for resource in resources: + self.process_recourse(writer, resource, hierarchy_level + 1) + if resources: + writer.writerow((None,) * len(self.HEADER)) + + def get_row(self, resource: ifcopenshell.entity_instance, hierarchy_level: int) -> Ifc2CsvRow: + resource_class = resource.is_a() + cost, unit = ifcopenshell.util.resource.get_cost(resource) + + quantity_name = None + labor_output, equipment_output = None, None + if resource_class in ("IfcConstructionEquipmentResource", "IfcLaborResource"): + pset: dict[str, Any] = ifcopenshell.util.element.get_pset(resource, "EPset_Productivity") + if pset: + quantity_name = cast(str, pset["BaseQuantityProducedName"]) + time_consumed: float = isodate.parse_duration(pset["BaseQuantityConsumed"]).total_seconds() / 3600 + produced: float = pset["BaseQuantityProducedValue"] + output = time_consumed / produced + if resource_class == "IfcLaborResource": + labor_output = output + else: + equipment_output, labor_output = None, output + + return Ifc2CsvRow( + hierarchy=hierarchy_level, + resource_type=self.inverse_resource_map[resource.is_a()], + name=resource.Name, + description=resource.Description, + cost=cost, + quantity_name=quantity_name, + labor_output=labor_output, + equipment_output=equipment_output, + guid=resource.GlobalId, + )