From d19d4fca3e7c685afb98015a38bca35f3f648539 Mon Sep 17 00:00:00 2001 From: bosonprojets Date: Thu, 6 May 2021 02:40:04 +0000 Subject: [PATCH] FEature to import Microsoft Project .xml Schedules and convert to IFC schema --- .../bim/module/sequence/__init__.py | 2 + .../bim/module/sequence/operator.py | 20 ++ src/ifcp6/ifcp6/msp2ifc.py | 207 ++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/ifcp6/ifcp6/msp2ifc.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 379532fa0c..bce21e7b78 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -61,6 +61,7 @@ classes = ( operator.UnassignProduct, operator.GenerateGanttChart, operator.ImportP6, + operator.ImportMSP, operator.LoadTaskProperties, operator.SelectTaskRelatedProducts, operator.VisualiseWorkScheduleDate, @@ -82,6 +83,7 @@ classes = ( def menu_func_import(self, context): self.layout.operator(operator.ImportP6.bl_idname, text="P6 (.xml)") + self.layout.operator(operator.ImportMSP.bl_idname, text="Microsoft Project (.xml)") def register(): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 88cdece0e5..f5ba11dc7d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -868,6 +868,26 @@ class ImportP6(bpy.types.Operator, ImportHelper): print("Import finished in {:.2f} seconds".format(time.time() - start)) return {"FINISHED"} +class ImportMSP(bpy.types.Operator, ImportHelper): + bl_idname = "import_msp.bim" + bl_label = "Import MSP" + filename_ext = ".xml" + filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"}) + + def execute(self, context): + from ifcp6.msp2ifc import MSP2Ifc + + self.file = IfcStore.get_file() + start = time.time() + msp2ifc = MSP2Ifc() + msp2ifc.xml = self.filepath + msp2ifc.file = self.file + msp2ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] if self.file.by_type("IfcWorkPlan") else None + msp2ifc.execute() + Data.load(IfcStore.get_file()) + 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" diff --git a/src/ifcp6/ifcp6/msp2ifc.py b/src/ifcp6/ifcp6/msp2ifc.py new file mode 100644 index 0000000000..8fbc80895b --- /dev/null +++ b/src/ifcp6/ifcp6/msp2ifc.py @@ -0,0 +1,207 @@ +import datetime +from datetime import timedelta +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.date +import xml.etree.ElementTree as ET +import blenderbim.bim.ifc + +class MSP2Ifc: + def __init__(self): + self.xml = None + self.file = None + self.ns = None + self.work_plan = None + self.project = {} + self.calendars = {} + self.wbs = {} + self.root_activites = [] + self.tasks = {} + self.relationships = {} + self.day_map = { + "2": 1, + "3": 2, + "4": 3, + "5": 4, + "6": 5, + "7": 6, + "1": 7, + } + + def execute(self): + self.parse_xml() + self.create_ifc() + + def parse_xml(self): + tree = ET.parse(self.xml) + project = tree.getroot() + self.ns = {"pr": project.tag[1:].partition("}")[0]} + self.project["Name"] = project.find("pr:Name", self.ns).text + self.parse_task_xml(project) + self.parse_calendar_xml(project) + + + def parse_task_xml(self, project): + for task in project.find("pr:Tasks", self.ns): + task_id = task.find("pr:UID", self.ns).text + task_index_level = task.find("pr:OutlineLevel", self.ns).text + wbs_id = task.find("pr:WBS", self.ns).text + relationship = task.find("pr:PredecessorLink", self.ns) + self.tasks[task_id] = { + "Name": task.find("pr:Name", self.ns).text, + "UID": task.find("pr:UID", self.ns).text, + "Start": datetime.datetime.fromisoformat(task.find("pr:Start", self.ns).text), + "Finish": datetime.datetime.fromisoformat(task.find("pr:Finish", self.ns).text), + "Duration": ifcopenshell.util.date.ifc2datetime(task.find("pr:Duration", self.ns).text), + "Priority": task.find("pr:Priority", self.ns).text, + "CalendarUID": task.find("pr:CalendarUID", self.ns).text, + "PredecessorTask": relationship.find("pr:PredecessorUID", self.ns).text if relationship else None, + "ifc": None, + } + + + def parse_calendar_xml(self, project): + for calendar in project.find("pr:Calendars", self.ns).findall("pr:Calendar", self.ns): + calendar_id = calendar.find("pr:UID", self.ns).text + week_days = [] + for week_day in calendar.find("pr:WeekDays", self.ns).findall( + "pr:WeekDay", self.ns + ): + working_times = [] + if week_day.find("pr:WorkingTimes", self.ns): + for working_time in week_day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns): + if working_time.find("pr:FromTime", self.ns) is None: + continue + working_times.append( + { + "Start": datetime.time.fromisoformat(working_time.find("pr:FromTime", self.ns).text), + "Finish": datetime.time.fromisoformat(working_time.find("pr:ToTime", self.ns).text), + } + ) + week_days.append( + { + "DayType": week_day.find("pr:DayType", self.ns).text, + "WorkingTimes": working_times, + "ifc": None, + } + ) + exceptions = {} + self.calendars[calendar_id] = { + "Name": calendar.find("pr:Name", self.ns).text, + "StandardWorkWeek": week_days, + } + + def create_ifc(self): + work_schedule = self.create_work_schedule() + self.create_tasks(work_schedule) + self.create_calendars() + self.create_rel_sequences() + + def create_tasks(self, work_schedule): + for task_id in self.tasks: + self.create_task_from_task(self.tasks[task_id], None, work_schedule) + + def create_work_schedule(self): + return ifcopenshell.api.run( + "sequence.add_work_schedule", self.file, name=self.project["Name"], work_plan=self.work_plan + ) + + def create_calendars(self): + for calendar in self.calendars.values(): + calendar["ifc"] = ifcopenshell.api.run( + "sequence.add_work_calendar", self.file, name=calendar["Name"] + ) + self.process_working_week(calendar["StandardWorkWeek"], calendar["ifc"]) + + def create_task_from_task(self, task, wbs, work_schedule): + task["ifc"] = ifcopenshell.api.run( + "sequence.add_task", + self.file, + work_schedule=None if wbs else work_schedule, + ) + ifcopenshell.api.run( + "sequence.edit_task", + self.file, + task=task["ifc"], + attributes={ + "Name": task["Name"], + "Identification": task["UID"], + "IsMilestone": task["Start"] == task["Finish"], + }, + ) + 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["Start"], + "ScheduleFinish": task["Finish"], + "DurationType": "WORKTIME" if task["Duration"] else None, + "ScheduleDuration": task["Duration"] if task["Duration"] else None, + }, + ) + + def process_working_week(self, week, calendar): + for day in week: + if day["ifc"]: + continue + + day["ifc"] = ifcopenshell.api.run( + "sequence.add_work_time", self.file, work_calendar=calendar, time_type="WorkingTimes" + ) + + weekday_component = [self.day_map[day["DayType"]]] + for day2 in week: + if day["DayType"] == day2["DayType"]: + continue + if day["WorkingTimes"] == day2["WorkingTimes"]: + weekday_component.append(self.day_map[day2["DayType"]]) + # Don't process the next day, as we can group it + day2["ifc"] = day["ifc"] + + work_time_name = "Weekdays: {}".format(", ".join([str(c) for c in sorted(weekday_component)])) + ifcopenshell.api.run( + "sequence.edit_work_time", + self.file, + work_time=day["ifc"], + attributes={"Name": work_time_name}, + ) + + recurrence = ifcopenshell.api.run( + "sequence.assign_recurrence_pattern", self.file, parent=day["ifc"], recurrence_type="WEEKLY" + ) + ifcopenshell.api.run( + "sequence.edit_recurrence_pattern", + self.file, + recurrence_pattern=recurrence, + attributes={"WeekdayComponent": weekday_component}, + ) + for work_time in day["WorkingTimes"]: + ifcopenshell.api.run( + "sequence.add_time_period", + self.file, + recurrence_pattern=recurrence, + start_time=work_time["Start"], + end_time=work_time["Finish"], + ) + + def create_rel_sequences(self): + self.sequence_type_map = { + "Start to Start": "START_START", + "Start to Finish": "START_FINISH", + "Finish to Start": "FINISH_START", + "Finish to Finish": "FINISH_FINISH", + } + for key, value in self.tasks.items(): + rel_sequence = ifcopenshell.api.run( + "sequence.assign_sequence", + self.file, + related_process = self.tasks[key]["ifc"], + relating_process = self.tasks[self.tasks[key]["PredecessorTask"]]["ifc"] + ) + ifcopenshell.api.run( + "sequence.edit_sequence", + self.file, + rel_sequence=rel_sequence, + )