From 8083a635a8bf7424cf1b45cfbbc532524022f369 Mon Sep 17 00:00:00 2001 From: Sigma Dimensions <79010126+myoualid@users.noreply.github.com> Date: Tue, 28 Mar 2023 04:04:48 +0000 Subject: [PATCH] You can now import work schedules from csv --- .../bim/module/sequence/__init__.py | 2 + .../bim/module/sequence/operator.py | 25 +++ src/ifc4d/ifc4d/construction_schedule.csv | 14 ++ src/ifc4d/ifc4d/csv4d2ifc.py | 201 ++++++++++++++++++ 4 files changed, 242 insertions(+) create mode 100644 src/ifc4d/ifc4d/construction_schedule.csv create mode 100644 src/ifc4d/ifc4d/csv4d2ifc.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 7a84e51b1d..8251d99589 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -81,6 +81,7 @@ classes = ( operator.GenerateGanttChart, operator.GuessDateRange, operator.ImportMSP, + operator.ImportCSV, operator.ImportP6, operator.ImportP6XER, operator.ImportPP, @@ -148,6 +149,7 @@ def menu_func_export(self, context): def menu_func_import(self, context): + self.layout.operator(operator.ImportCSV.bl_idname, text="Work Schedule (.csv)") self.layout.operator(operator.ImportP6.bl_idname, text="P6 (.xml)") self.layout.operator(operator.ImportP6XER.bl_idname, text="P6 (.xer)") self.layout.operator(operator.ImportPP.bl_idname, text="Powerproject (.pp)") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index e55554e3fb..0e6502eb1d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -527,6 +527,31 @@ class DisableEditingWorkCalendar(bpy.types.Operator): return {"FINISHED"} +class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): + bl_idname = "import_csv.bim" + bl_label = "Import CSV" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".csv" + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + @classmethod + def poll(cls, context): + ifc_file = IfcStore.get_file() + return ifc_file is not None + + def execute(self, context): + from ifc4d.csv4d2ifc import Csv2Ifc + + self.file = tool.Ifc.get() + start = time.time() + csv2ifc = Csv2Ifc() + csv2ifc.csv = self.filepath + csv2ifc.file = self.file + csv2ifc.execute() + print("Imported in %s seconds" % (time.time() - start)) + return {"FINISHED"} + + class ImportP6(bpy.types.Operator, ImportHelper): bl_idname = "import_p6.bim" bl_label = "Import P6" diff --git a/src/ifc4d/ifc4d/construction_schedule.csv b/src/ifc4d/ifc4d/construction_schedule.csv new file mode 100644 index 0000000000..1d60897625 --- /dev/null +++ b/src/ifc4d/ifc4d/construction_schedule.csv @@ -0,0 +1,14 @@ +Hierarchy,Identification,Name,ScheduleStart,ScheduleFinish,ScheduleDuration,ActualStart,ActualFinish,ActualDuration,Relationships +1,1,Demolition,,,,,,, +,,,,,,,,, +2,1.1,Building A,,,,,,, +,,,,,,,,, +3,1.1.1,Soft strip,01/01/23,,5d,07/01/23,10/01/23,,111FS112 +3,1.1.2,Hard Strip,,,5d,,,,112FS113 +3,1.1.3,Hazmat,,,7d,,,,113FS121 +,,,,,,,,, +2,1.2,Building B,,,,,,, +,,,,,,,,, +3,1.2.1,Soft strip,,,3d,,,,121FS122 +3,1.2.2,Hard Strip,,,4h,,,,122FS123 +3,1.2.3,Hazmat,,,2d16h,,,, diff --git a/src/ifc4d/ifc4d/csv4d2ifc.py b/src/ifc4d/ifc4d/csv4d2ifc.py new file mode 100644 index 0000000000..79f551fa47 --- /dev/null +++ b/src/ifc4d/ifc4d/csv4d2ifc.py @@ -0,0 +1,201 @@ +# Ifc4D - IFC scheduling utility +# Copyright (C) 2021-2022 Dion Moult , Yassine Oualid +# +# This file is part of Ifc4D. +# +# Ifc4D is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ifc4D is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ifc4D. If not, see . + + +import csv +import ifcopenshell +import ifcopenshell.api +import locale +import re + + +class Csv2Ifc: + def __init__(self): + self.csv = None + self.file = None + self.tasks = [] + self.work_schedule = None + self.work_plan = None + self.is_schedule_of_rates = False + self.units = {} + + def execute(self): + self.parse_csv() + self.create_ifc() + + def parse_csv(self): + self.parents = {} + self.headers = {} + locale.setlocale(locale.LC_ALL, "") # set the system locale + with open(self.csv, "r", encoding="ISO-8859-1") as csv_file: + reader = csv.reader(csv_file) + for row in reader: + if not row[0]: + continue + if row[0] == "Hierarchy": + for i, col in enumerate(row): + if not col: + continue + self.headers[col] = i + continue + task = self.get_row_task(row) + hierarchy_key = int(row[0]) + if hierarchy_key == 1: + self.tasks.append(task) + else: + self.parents[hierarchy_key - 1]["children"].append(task) + self.parents[hierarchy_key] = task + + def parse_task_rel(self, task_relationships_str): + rels = [] + if task_relationships_str: + for rel_str in task_relationships_str.split(";"): + rel_parts = re.split(r"(\d+)", rel_str) + task_1 = rel_parts[1] + rel_type = rel_parts[2] + task_2 = rel_parts[3] + rels.append( + { + "rel_type": rel_type, + "task_1": task_1, + "task_2": task_2, + } + ) + return rels + + def get_row_task(self, row): + hours_per_day = 8 + hierarchy = row[self.headers["Hierarchy"]] + identification = row[self.headers["Identification"]] + task_name = row[self.headers["Name"]] + + task_relationships = self.parse_task_rel(row[self.headers["Relationships"]]) + + scheduled_start_date = ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleStart"]]) + scheduled_finish_date = ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleFinish"]]) + scheduled_duration = ifcopenshell.util.date.string_to_duration(row[self.headers["ScheduleDuration"]]) + actual_start_date = ifcopenshell.util.date.string_to_date(row[self.headers["ActualStart"]]) + actual_finish_date = ifcopenshell.util.date.string_to_date(row[self.headers["ActualFinish"]]) + actual_duration = ifcopenshell.util.date.string_to_duration(row[self.headers["ActualDuration"]]) + + return { + "Hierarchy": hierarchy, + "Identification": identification, + "Name": task_name, + "Relationships": task_relationships, + "ScheduleStart": scheduled_start_date, + "ScheduleFinish": scheduled_finish_date, + "ScheduleDuration": scheduled_duration, + "ActualStart": actual_start_date, + "ActualFinish": actual_finish_date, + "ActualDuration": actual_duration, + "children": [], + } + + def create_ifc(self): + if not self.file: + self.create_boilerplate_ifc() + if not self.work_plan: + self.work_plan = ifcopenshell.api.run("sequence.add_work_plan", self.file) + self.work_schedule = self.create_work_schedule() + self.create_tasks(self.tasks) + self.create_rel_sequences(self.tasks) + + def create_boilerplate_ifc(self): + self.file = ifcopenshell.file(schema="IFC4") + self.work_plan = self.file.create_entity("IfcWorkPlan") + + def create_tasks(self, tasks, parent=None): + for task in tasks: + self.create_task(task, parent) + + def create_rel_sequences(self, tasks=None): + self.sequence_type_map = { + "FF": "FINISH_FINISH", + "FS": "FINISH_START", + "SF": "START_FINISH", + "SS": "START_START", + } + for task in tasks: + self.create_rel_sequence(task) + + def create_work_schedule(self): + return ifcopenshell.api.run("sequence.add_work_schedule", self.file, name="import", work_plan=self.work_plan) + + def create_task(self, task, parent_task=None): + if parent_task is None: + task["ifc"] = ifcopenshell.api.run("sequence.add_task", self.file, work_schedule=self.work_schedule) + else: + task["ifc"] = ifcopenshell.api.run("sequence.add_task", self.file, parent_task=parent_task) + + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + task=task["ifc"], + attributes={ + "Name": task["Name"], + "Identification": task["Identification"], + # "IsMilestone": task["ScheduleStart"] == task["ScheduleFinish"], + }, + ) + task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task["ifc"]) + ifcopenshell.api.run( + "sequence.edit_task_time", + self.file, + task_time=task_time, + attributes={ + "ScheduleStart": task["ScheduleStart"], + "ScheduleFinish": task["ScheduleFinish"], + "DurationType": "WORKTIME" if task["ScheduleDuration"] else None, + "ScheduleDuration": task["ScheduleDuration"] if task["ScheduleDuration"] else None, + "ActualStart": task["ActualStart"], + "ActualFinish": task["ActualFinish"], + "ActualDuration": task["ActualDuration"], + }, + ) + self.create_tasks(task["children"], task["ifc"]) + + def get_entity_by_identification(self, tasks, task_identification): + for task in tasks or []: + if task["Identification"].replace(".", "").replace(" ", "") == task_identification.replace(" ", ""): + return task["ifc"] + else: + entity = self.get_entity_by_identification(task["children"], task_identification) + if entity: + return entity + return None + + def create_rel_sequence(self, task=None): + for rel in task["Relationships"] or []: + task_1 = self.get_entity_by_identification(self.tasks, rel["task_1"]) + task_2 = self.get_entity_by_identification(self.tasks, rel["task_2"]) + rel_type = self.sequence_type_map[rel["rel_type"]] + rel_sequence = ifcopenshell.api.run( + "sequence.assign_sequence", + self.file, + related_process=task_2, + relating_process=task_1, + ) + if rel_type: + ifcopenshell.api.run( + "sequence.edit_sequence", + self.file, + rel_sequence=rel_sequence, + attributes={"SequenceType": rel_type}, + ) + self.create_rel_sequences(task["children"])