From 114214e2a18d04335fa89036b3e56d250200e37e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 6 May 2021 14:23:20 +1000 Subject: [PATCH] Implement natural sorting for task trees, and support nested tasks in MS Project XML imports --- .../bim/module/sequence/operator.py | 16 ++++++--- src/ifcp6/ifcp6/msp2ifc.py | 35 ++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index f5ba11dc7d..dfa99cd789 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,3 +1,4 @@ +import re import os import bpy import json @@ -258,10 +259,11 @@ class EnableEditingTasks(bpy.types.Operator): self.tprops.tasks.remove(0) self.contracted_tasks = json.loads(self.props.contracted_tasks) - sort_keys = { + self.sort_keys = { i: Data.tasks[i]["Identification"] for i in Data.work_schedules[self.work_schedule]["RelatedObjects"] } - for related_object_id in sorted(sort_keys, key=sort_keys.__getitem__): + + for related_object_id in sorted(self.sort_keys, key=self.natural_sort_key): self.create_new_task_li(related_object_id, 0) bpy.ops.bim.load_task_properties() self.props.editing_type = "TASKS" @@ -276,10 +278,13 @@ class EnableEditingTasks(bpy.types.Operator): if task["RelatedObjects"]: new.has_children = True if new.is_expanded: - sort_keys = {i: Data.tasks[i]["Identification"] for i in task["RelatedObjects"]} - for related_object_id in sorted(sort_keys, key=sort_keys.__getitem__): + self.sort_keys = {i: Data.tasks[i]["Identification"] for i in task["RelatedObjects"]} + for related_object_id in sorted(self.sort_keys, key=self.natural_sort_key): self.create_new_task_li(related_object_id, level_index + 1) - return {"FINISHED"} + + def natural_sort_key(self, i, _nsre=re.compile("([0-9]+)")): + s = self.sort_keys[i] + return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)] class LoadTaskProperties(bpy.types.Operator): @@ -868,6 +873,7 @@ 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" diff --git a/src/ifcp6/ifcp6/msp2ifc.py b/src/ifcp6/ifcp6/msp2ifc.py index 8fbc80895b..e5ac083e29 100644 --- a/src/ifcp6/ifcp6/msp2ifc.py +++ b/src/ifcp6/ifcp6/msp2ifc.py @@ -37,6 +37,8 @@ class MSP2Ifc: project = tree.getroot() self.ns = {"pr": project.tag[1:].partition("}")[0]} self.project["Name"] = project.find("pr:Name", self.ns).text + self.outline_level = 0 + self.outline_parents = {} self.parse_task_xml(project) self.parse_calendar_xml(project) @@ -47,15 +49,25 @@ class MSP2Ifc: 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) + outline_level = int(task.find("pr:OutlineLevel", self.ns).text) + + if outline_level != 0: + parent_task = self.tasks[self.outline_parents[outline_level-1]] + parent_task["subtasks"].append(task_id) + self.outline_level = outline_level + self.outline_parents[outline_level] = task_id + self.tasks[task_id] = { "Name": task.find("pr:Name", self.ns).text, - "UID": task.find("pr:UID", self.ns).text, + "OutlineNumber": task.find("pr:OutlineNumber", self.ns).text, + "OutlineLevel": outline_level, "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, + "subtasks": [], "ifc": None, } @@ -99,7 +111,9 @@ class MSP2Ifc: def create_tasks(self, work_schedule): for task_id in self.tasks: - self.create_task_from_task(self.tasks[task_id], None, work_schedule) + task = self.tasks[task_id] + if task["OutlineLevel"] == 0: + self.create_task(task, work_schedule=work_schedule) def create_work_schedule(self): return ifcopenshell.api.run( @@ -113,11 +127,12 @@ class MSP2Ifc: ) self.process_working_week(calendar["StandardWorkWeek"], calendar["ifc"]) - def create_task_from_task(self, task, wbs, work_schedule): + def create_task(self, task, work_schedule=None, parent_task=None): task["ifc"] = ifcopenshell.api.run( "sequence.add_task", self.file, - work_schedule=None if wbs else work_schedule, + work_schedule=work_schedule if work_schedule else None, + parent_task=parent_task["ifc"] if parent_task else None, ) ifcopenshell.api.run( "sequence.edit_task", @@ -125,7 +140,7 @@ class MSP2Ifc: task=task["ifc"], attributes={ "Name": task["Name"], - "Identification": task["UID"], + "Identification": task["OutlineNumber"], "IsMilestone": task["Start"] == task["Finish"], }, ) @@ -141,6 +156,8 @@ class MSP2Ifc: "ScheduleDuration": task["Duration"] if task["Duration"] else None, }, ) + for subtask_id in task["subtasks"]: + self.create_task(self.tasks[subtask_id], parent_task=task) def process_working_week(self, week, calendar): for day in week: @@ -193,12 +210,14 @@ class MSP2Ifc: "Finish to Start": "FINISH_START", "Finish to Finish": "FINISH_FINISH", } - for key, value in self.tasks.items(): + for task in self.tasks.values(): + if not task["PredecessorTask"]: + continue 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"] + related_process = task["ifc"], + relating_process = self.tasks[task["PredecessorTask"]]["ifc"] ) ifcopenshell.api.run( "sequence.edit_sequence",