From 01f25528e8936c9e4c7ad8d0146f8d1862be2106 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 25 Jan 2022 16:44:37 +1100 Subject: [PATCH] Build untested IFC to P6 XML export feature. --- .../bim/module/sequence/__init__.py | 149 ++++++++------- .../bim/module/sequence/operator.py | 25 +++ src/ifc4d/README.md | 5 +- src/ifc4d/ifc4d/ifc2p6.py | 176 ++++++++++++++++++ 4 files changed, 282 insertions(+), 73 deletions(-) create mode 100644 src/ifc4d/ifc4d/ifc2p6.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 18960b8fe0..da8cc91898 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -20,86 +20,87 @@ import bpy from . import ui, prop, operator classes = ( - operator.AddWorkPlan, - operator.EditWorkPlan, - operator.RemoveWorkPlan, - operator.EnableEditingWorkPlan, - operator.DisableEditingWorkPlan, - operator.EnableEditingWorkPlanSchedules, - operator.AssignWorkSchedule, - operator.UnassignWorkSchedule, - operator.AddWorkSchedule, - operator.EditWorkSchedule, - operator.RemoveWorkSchedule, - operator.EnableEditingWorkSchedule, - operator.EnableEditingTasks, - operator.DisableEditingWorkSchedule, - operator.DisableEditingSequence, - operator.EditSequenceAttributes, - operator.EditSequenceTimeLag, - operator.EnableEditingSequenceAttributes, - operator.EnableEditingSequenceTimeLag, + operator.AddSummaryTask, + operator.AddTask, + operator.AddTaskColumn, + operator.AddTimePeriod, operator.AddWorkCalendar, - operator.EditWorkCalendar, - operator.EditWorkTime, - operator.RemoveWorkCalendar, - operator.RemoveWorkTime, - operator.UnassignRecurrencePattern, - operator.UnassignLagTime, - operator.RemoveTimePeriod, - operator.EnableEditingWorkCalendar, - operator.EnableEditingWorkTime, - operator.EnableEditingWorkCalendarTimes, - operator.DisableEditingWorkCalendar, - operator.DisableEditingWorkTime, + operator.AddWorkPlan, + operator.AddWorkSchedule, operator.AddWorkTime, operator.AssignLagTime, - operator.AssignRecurrencePattern, - operator.AddTimePeriod, - operator.AddTask, - operator.AddSummaryTask, - operator.ExpandTask, - operator.ContractTask, - operator.RemoveTask, - operator.EnableEditingTask, - operator.DisableEditingTask, - operator.DisableEditingTaskTime, - operator.EditTask, - operator.CopyTaskAttribute, operator.AssignPredecessor, - operator.AssignSuccessor, - operator.UnassignPredecessor, - operator.UnassignSuccessor, - operator.EnableEditingTaskTime, - operator.EnableEditingTaskCalendar, - operator.EnableEditingTaskSequence, - operator.EditTaskTime, - operator.EditTaskCalendar, - operator.RemoveTaskCalendar, - operator.AssignProduct, - operator.UnassignProduct, operator.AssignProcess, - operator.UnassignProcess, - operator.GenerateGanttChart, - operator.ImportP6, - operator.ImportP6XER, - operator.ImportPP, - operator.ImportMSP, - operator.LoadTaskProperties, - operator.SelectTaskRelatedProducts, - operator.VisualiseWorkScheduleDate, - operator.VisualiseWorkScheduleDateRange, - operator.RecalculateSchedule, + operator.AssignProduct, + operator.AssignRecurrencePattern, + operator.AssignSuccessor, + operator.AssignWorkSchedule, operator.BlenderBIM_DatePicker, operator.BlenderBIM_DatePickerSetDate, operator.BlenderBIM_RedrawDatePicker, - operator.AddTaskColumn, - operator.RemoveTaskColumn, - operator.SetTaskSortColumn, - operator.LoadTaskInputs, - operator.LoadTaskResources, - operator.LoadTaskOutputs, operator.CalculateTaskDuration, + operator.ContractTask, + operator.CopyTaskAttribute, + operator.DisableEditingSequence, + operator.DisableEditingTask, + operator.DisableEditingTaskTime, + operator.DisableEditingWorkCalendar, + operator.DisableEditingWorkPlan, + operator.DisableEditingWorkSchedule, + operator.DisableEditingWorkTime, + operator.EditSequenceAttributes, + operator.EditSequenceTimeLag, + operator.EditTask, + operator.EditTaskCalendar, + operator.EditTaskTime, + operator.EditWorkCalendar, + operator.EditWorkPlan, + operator.EditWorkSchedule, + operator.EditWorkTime, + operator.EnableEditingSequenceAttributes, + operator.EnableEditingSequenceTimeLag, + operator.EnableEditingTask, + operator.EnableEditingTaskCalendar, + operator.EnableEditingTaskSequence, + operator.EnableEditingTaskTime, + operator.EnableEditingTasks, + operator.EnableEditingWorkCalendar, + operator.EnableEditingWorkCalendarTimes, + operator.EnableEditingWorkPlan, + operator.EnableEditingWorkPlanSchedules, + operator.EnableEditingWorkSchedule, + operator.EnableEditingWorkTime, + operator.ExpandTask, + operator.ExportP6, + operator.GenerateGanttChart, + operator.ImportMSP, + operator.ImportP6, + operator.ImportP6XER, + operator.ImportPP, + operator.LoadTaskInputs, + operator.LoadTaskOutputs, + operator.LoadTaskProperties, + operator.LoadTaskResources, + operator.RecalculateSchedule, + operator.RemoveTask, + operator.RemoveTaskCalendar, + operator.RemoveTaskColumn, + operator.RemoveTimePeriod, + operator.RemoveWorkCalendar, + operator.RemoveWorkPlan, + operator.RemoveWorkSchedule, + operator.RemoveWorkTime, + operator.SelectTaskRelatedProducts, + operator.SetTaskSortColumn, + operator.UnassignLagTime, + operator.UnassignPredecessor, + operator.UnassignProcess, + operator.UnassignProduct, + operator.UnassignRecurrencePattern, + operator.UnassignSuccessor, + operator.UnassignWorkSchedule, + operator.VisualiseWorkScheduleDate, + operator.VisualiseWorkScheduleDateRange, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, @@ -124,6 +125,10 @@ classes = ( ) +def menu_func_export(self, context): + self.layout.operator(operator.ExportP6.bl_idname, text="P6 (.xml)") + + def menu_func_import(self, context): self.layout.operator(operator.ImportP6.bl_idname, text="P6 (.xml)") self.layout.operator(operator.ImportP6XER.bl_idname, text="P6 (.xer)") @@ -138,6 +143,7 @@ def register(): bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) bpy.types.Scene.DatePickerProperties = bpy.props.PointerProperty(type=prop.DatePickerProperties) bpy.types.TextCurve.BIMDateTextProperties = bpy.props.PointerProperty(type=prop.BIMDateTextProperties) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) @@ -148,4 +154,5 @@ def unregister(): del bpy.types.Scene.BIMWorkCalendarProperties del bpy.types.Scene.DatePickerProperties del bpy.types.TextCurve.BIMDateTextProperties + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index d37ec2ff32..bf54bbf0f2 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1196,6 +1196,31 @@ class ImportMSP(bpy.types.Operator, ImportHelper): return {"FINISHED"} +class ExportP6(bpy.types.Operator, ImportHelper): + bl_idname = "export_p6.bim" + bl_label = "Export P6" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".xml" + filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"}) + + @classmethod + def poll(cls, context): + ifc_file = IfcStore.get_file() + return ifc_file is not None + + def execute(self, context): + from ifc4d.ifc2p6 import Ifc2P6 + + self.file = IfcStore.get_file() + start = time.time() + ifc2p6 = Ifc2P6() + ifc2p6.xml = self.filepath + ifc2p6.file = self.file + ifc2p6.execute() + print("Import finished in {:.2f} seconds".format(time.time() - start)) + return {"FINISHED"} + + class EnableEditingWorkCalendarTimes(bpy.types.Operator): bl_idname = "bim.enable_editing_work_calendar_times" bl_label = "Enable Editing Work Calendar Times" diff --git a/src/ifc4d/README.md b/src/ifc4d/README.md index c774d01f07..658eadc531 100644 --- a/src/ifc4d/README.md +++ b/src/ifc4d/README.md @@ -6,17 +6,18 @@ Ifc4D contains a series of utilities for converting to and from various 4D softw - Oracle Primavera 6 (P6) XML to IFC - Oracle Primavera 6 (P6) XER to IFC - Asta Powerproject to IFC + - IFC to Oracle Primavera 6 (P6) XML Planned (would you like to contribute? Please reach out!): - IFC to Microsoft Project - - IFC to Oracle Primavera 6 (P6) + - IFC to Oracle Primavera 6 (P6) XER + - IFC to Oracle Primavera 6 (P6) XLS - IFC to Asta Powerproject - LibreProject to IFC - IFC to LibreProject - IFC to Gantt - ## Useful links - [P6 EPPM XER Import/Export Data Map Guide](https://docs.oracle.com/cd/F12057_01/English/Mapping_and_Schema/xer_import_export_data_map_project/helpmain.htm?toc.htm?97881.htm) diff --git a/src/ifc4d/ifc4d/ifc2p6.py b/src/ifc4d/ifc4d/ifc2p6.py new file mode 100644 index 0000000000..ece1c20936 --- /dev/null +++ b/src/ifc4d/ifc4d/ifc2p6.py @@ -0,0 +1,176 @@ +# Ifc4D - IFC scheduling utility +# Copyright (C) 2022 Dion Moult +# +# 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 math +import datetime +import ifcopenshell +import ifcopenshell.util.date +import xml.etree.ElementTree as ET +from .common import ScheduleIfcGenerator + + +class Ifc2P6: + def __init__(self): + self.xml = None + self.file = None + self.id = 0 + self.id_map = {} + self.element_map = {} + self.hours_per_day = 8 + self.project_tasks = [] + + def execute(self): + self.root = ET.Element("APIBusinessObjects") + self.schedule = self.file.by_type("IfcWorkSchedule")[0] + + self.create_obs() + self.create_calendars() + self.create_projects() + + tree = ET.ElementTree(self.root) + tree.write(self.xml) + + def create_obs(self): + element = self.file.by_type("IfcProject")[0] + obs = ET.SubElement(self.root, "OBS") + self.link_element(element, obs) + ET.SubElement(obs, "Name").text = element.Name or "Unnamed" + + def create_calendars(self): + for calendar in self.file.by_type("IfcWorkCalendar"): + self.create_calendar(calendar) + + def create_calendar(self, calendar): + el = ET.SubElement(self.root, "Calendar") + ET.SubElement(el, "Name").text = calendar.Name or "Unnamed" + self.link_element(calendar, el) + + # Hardcode for now + work_week = ET.SubElement(el, "StandardWorkWeek") + + work_hours = ET.SubElement(work_week, "StandardWorkHours") + day = ET.SubElement(work_hours, "DayOfWeek").text = "Sunday" + + for name in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]: + work_hours = ET.SubElement(work_week, "StandardWorkHours") + day = ET.SubElement(work_hours, "DayOfWeek").text = name + work_time = ET.SubElement(work_hours, "WorkTime") + ET.SubElement(work_time, "Start").text = "08:00:00" + ET.SubElement(work_time, "Finish").text = "17:00:00" + + holidays = ET.SubElement(el, "HolidayOrExceptions") + + holiday = ET.SubElement(holidays, "HolidayOrException") + date = ET.SubElement(holidays, "Date").text = "2022-01-01T00:00:00" + + holiday = ET.SubElement(holidays, "HolidayOrException") + date = ET.SubElement(holidays, "Date").text = "2022-01-02T00:00:00" + + holiday = ET.SubElement(holidays, "HolidayOrException") + date = ET.SubElement(holidays, "Date").text = "2022-01-03T00:00:00" + + def create_projects(self): + for work_schedule in self.file.by_type("IfcWorkSchedule"): + self.create_project(work_schedule) + + def create_project(self, work_schedule): + project = ET.SubElement(self.root, "Project") + self.link_element(work_schedule, project) + + ET.SubElement(project, "ActivityDefaultCalendarObjectId").text = self.id_map[self.file.by_type("IfcWorkCalendar")[0]] + ET.SubElement(project, "Name").text = work_schedule.Name or "Unnamed" + ET.SubElement(project, "OBSObjectId").text = self.id_map[self.file.by_type("IfcProject")[0]] + + self.project_tasks = [] + self.create_wbs(self.get_subtasks(work_schedule), work_schedule) + self.create_relationships(self.project_tasks, work_schedule) + + def create_wbs(self, tasks, work_schedule, parent=None): + self.project_tasks.extend(tasks) + for task in tasks: + subtasks = self.get_subtasks(task) + if not subtasks: + self.create_activity(task, work_schedule, parent=parent) + continue + wbs = ET.SubElement(self.element_map[work_schedule], "WBS") + self.link_element(task, wbs) + ET.SubElement(wbs, "Code").text = task.Identification or "X" + ET.SubElement(wbs, "Name").text = task.Name or "Unnamed" + if parent: + ET.SubElement(wbs, "ParentObjectId").text = self.id_map[parent] + ET.SubElement(wbs, "ProjectObjectId").text = self.id_map[work_schedule] + self.create_wbs(subtasks, work_schedule, parent=task) + + def create_activity(self, task, work_schedule, parent=None): + activity = ET.SubElement(self.element_map[work_schedule], "Activity") + self.link_element(task, activity) + ET.SubElement(activity, "Id").text = task.Identification or "X" + ET.SubElement(activity, "Name").text = task.Name or "Unnamed" + if parent: + ET.SubElement(activity, "WBSObjectId").text = self.id_map[parent] + if task.TaskTime: + if task.TaskTime.ScheduleDuration: + duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration) + ET.SubElement(activity, "PlannedDuration").text = str(duration.days * self.hours_per_day) + if task.TaskTime.ScheduleStart: + ET.SubElement(activity, "PlannedStartDate").text = task.TaskTime.ScheduleStart + ET.SubElement(activity, "StartDate").text = task.TaskTime.ScheduleStart + if task.TaskTime.ScheduleFinish: + ET.SubElement(activity, "PlannedFinishDate").text = task.TaskTime.ScheduleFinish + ET.SubElement(activity, "FinishDate").text = task.TaskTime.ScheduleFinish + ET.SubElement(activity, "ProjectObjectId").text = self.id_map[work_schedule] + + def create_relationships(self, tasks, work_schedule): + for task in tasks: + for rel in task.IsSuccessorFrom or []: + predecessor = rel.RelatingProcess + relationship = ET.SubElement(self.element_map[work_schedule], "Relationship") + self.link_element(rel, relationship) + if rel.TimeLag and rel.TimeLag.LagValue: + duration = ifcopenshell.util.date.ifc2datetime(rel.TimeLag.LagValue.wrappedValue) + ET.SubElement(relationship, "Lag").text = str(duration.days * self.hours_per_day) + else: + ET.SubElement(relationship, "Lag").text = "0" + ET.SubElement(relationship, "PredecessorProjectObjectId").text = self.id_map[work_schedule] + ET.SubElement(relationship, "SuccessorProjectObjectId").text = self.id_map[work_schedule] + ET.SubElement(relationship, "PredecessorActivityObjectId").text = self.id_map[predecessor] + ET.SubElement(relationship, "SuccessorActivityObjectId").text = self.id_map[task] + ET.SubElement(relationship, "Type").text = { + "START_START": "Start to Start", + "START_FINISH": "Start to Finish", + "FINISH_START": "Finish to Start", + "FINISH_FINISH": "Finish to Finish", + None: "Finish to Start", + }[rel.SequenceType] + + def get_subtasks(self, element): + tasks = [] + if element.is_a("IfcWorkSchedule"): + for rel in element.Controls or []: + tasks.extend([e for e in rel.RelatedObjects if e.is_a("IfcTask")]) + elif element.is_a("IfcTask"): + for rel in element.IsNestedBy or []: + tasks.extend([e for e in rel.RelatedObjects if e.is_a("IfcTask")]) + return tasks + + def link_element(self, element, el): + self.id += 1 + ET.SubElement(el, "ObjectId").text = str(self.id) + self.id_map[element] = str(self.id) + self.element_map[element] = el + return self.id