diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 833e02bf6f..18960b8fe0 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -83,6 +83,7 @@ classes = ( operator.GenerateGanttChart, operator.ImportP6, operator.ImportP6XER, + operator.ImportPP, operator.ImportMSP, operator.LoadTaskProperties, operator.SelectTaskRelatedProducts, @@ -126,6 +127,7 @@ classes = ( 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)") + self.layout.operator(operator.ImportPP.bl_idname, text="Powerproject (.pp)") self.layout.operator(operator.ImportMSP.bl_idname, text="Microsoft Project (.xml)") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 2c27aabbdf..ba7e507a65 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1120,6 +1120,27 @@ class ImportP6XER(bpy.types.Operator, ImportHelper): return {"FINISHED"} +class ImportPP(bpy.types.Operator, ImportHelper): + bl_idname = "import_pp.bim" + bl_label = "Import Powerproject pp" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".pp" + filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"}) + + def execute(self, context): + from ifc4d.pp2ifc import PP2Ifc + + self.file = IfcStore.get_file() + start = time.time() + pp2ifc = PP2Ifc() + pp2ifc.pp = self.filepath + pp2ifc.file = self.file + pp2ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] if self.file.by_type("IfcWorkPlan") else None + pp2ifc.execute() + Data.load(IfcStore.get_file()) + 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/ifc4d/ifc4d/__main__.py b/src/ifc4d/__main__.py similarity index 85% rename from src/ifc4d/ifc4d/__main__.py rename to src/ifc4d/__main__.py index cca02078f3..db69d96a3f 100644 --- a/src/ifc4d/ifc4d/__main__.py +++ b/src/ifc4d/__main__.py @@ -2,9 +2,10 @@ import os import sys import argparse -from p6xer2ifc import P6XER2Ifc -from p62ifc import P62Ifc -from msp2ifc import MSP2Ifc +from ifc4d.p6xer2ifc import P6XER2Ifc +from ifc4d.p62ifc import P62Ifc +from ifc4d.msp2ifc import MSP2Ifc +from ifc4d.pp2ifc import PP2Ifc import ifcopenshell @@ -12,7 +13,7 @@ parser = argparse.ArgumentParser() parser .add_argument('-f','--file', action='store', type=str, required=True, help="schedule file name to be parsed") parser .add_argument('-s','--schedule', action='store', required=True, - type=str, help='file format as xer, p6xml, mspxml') + type=str, help='file format as xer, p6xml, mspxml, pp') parser .add_argument('-i', '--ifcfile', action='store', required=False, type=str, help='ifc file name as string e.g. \"file.ifc\"') parser .add_argument('-o', '--output', action='store', required=True, @@ -65,6 +66,14 @@ elif args.schedule == "p6xml": if ifcfile: p6xml.file = ifcopenshell.open(args.ifcfile) p6xml.execute() +elif args.schedule == "pp": + pp = PP2Ifc() + pp.output = args.output + pp.pp = args.file + ifcfile = get_file() + if ifcfile: + pp.file = ifcopenshell.open(args.ifcfile) + pp.execute() else: print("schedule type you selected is not implemented at the moment") diff --git a/src/ifc4d/ifc4d/common.py b/src/ifc4d/ifc4d/common.py index a43284273c..ed2314c99d 100644 --- a/src/ifc4d/ifc4d/common.py +++ b/src/ifc4d/ifc4d/common.py @@ -200,12 +200,12 @@ class ScheduleIfcGenerator: identification = wbs["Code"] if wbs["ParentObjectId"]: if self.wbs[wbs["ParentObjectId"]]["ifc"]: - identification = self.wbs[wbs["ParentObjectId"]]["ifc"].Identification + "." + wbs["Code"] + identification = str(self.wbs[wbs["ParentObjectId"]]["ifc"].Identification) + "." + str(wbs["Code"]) ifcopenshell.api.run( "sequence.edit_task", self.file, task=wbs["ifc"], - attributes={"Name": wbs["Name"], "Identification": identification}, + attributes={"Name": wbs["Name"], "Identification": str(identification)}, ) for activity_id in wbs["activities"]: self.create_task_from_activity(self.activities[activity_id], wbs, None) @@ -223,7 +223,7 @@ class ScheduleIfcGenerator: task=activity["ifc"], attributes={ "Name": activity["Name"], - "Identification": activity["Identification"], + "Identification": str(activity["Identification"]), "Status": activity["Status"], "IsMilestone": activity["StartDate"] == activity["FinishDate"], "PredefinedType": "CONSTRUCTION" diff --git a/src/ifc4d/ifc4d/p6xer2ifc.py b/src/ifc4d/ifc4d/p6xer2ifc.py index d99538f2b2..71403c3917 100644 --- a/src/ifc4d/ifc4d/p6xer2ifc.py +++ b/src/ifc4d/ifc4d/p6xer2ifc.py @@ -193,3 +193,4 @@ class P6XER2Ifc(): # TODO: consider showing progress bar for better user experience # TODO: support multiple projects in a single file # TODO: prompt user to select activities and/or wbs nodes to import instead of the full project + diff --git a/src/ifc4d/ifc4d/pp2ifc.py b/src/ifc4d/ifc4d/pp2ifc.py new file mode 100644 index 0000000000..9e8a8068ac --- /dev/null +++ b/src/ifc4d/ifc4d/pp2ifc.py @@ -0,0 +1,217 @@ +print("In module products __package__, __name__ ==", __package__) +print(__name__) +import sys +print("In module products sys.path[0], __package__ ==", sys.path[0], __package__) +sys.path.append(sys.path[0]) + +import sqlite3 +import math +import datetime +from datetime import timedelta +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.date +from .wpattern import AstaCalendarWorkPattern +from .common import ScheduleIfcGenerator +import time + + + +list_of_tables = [ + 'BAR', + 'EXPANDED_TASK', + 'PERMANENT_RESOURCE', + 'CONSUMABLE_RESOURCE', + 'TASK', + 'PROJECT_SUMMARY', + 'WBS_ENTRY' +] + +class PP2Ifc: + def __init__(self): + + self.pp = None + self.file = None + self.work_plan = None + self.project = {} + self.calendars = {} + self.wbs = {} + self.root_activites = [] + self.activities = {} + self.relationships = {} + self.resources = {} + self.output = None + self.day_map = { + "Monday": 1, + "Tuesday": 2, + "Wednesday": 3, + "Thursday": 4, + "Friday": 5, + "Saturday": 6, + "Sunday": 7, + } + + self.relationship_map = { + 0: "FINISH_START", + 1: "FINISH_FINISH", + 2: "START_START", + 3: "START_FINISH" + } + + + + def get_json(self, table_name): + self.cur.execute("select * from " + table_name) + r = [dict((self.cur.description[i][0], value) for i, value in enumerate(row)) for row in self.cur.fetchall()] + return r + + def get_json_with_filter(self, table_name, attr_name, attr_value): + self.cur.execute("select * from " + table_name + " where " + attr_name + " = " + str(attr_value)) + r = [dict((self.cur.description[i][0], value) for i, value in enumerate(row)) for row in self.cur.fetchall()] + return r + + def execute(self): + self.con = sqlite3.connect(self.pp) + self.cur = self.con.cursor() + self.parse_pp() + settings = { + "work_plan":self.work_plan, + "project": self.project, + "calendars": self.calendars, + "wbs": self.wbs, + "root_activities": self.root_activites, + "activities": self.activities, + "relationships": self.relationships, + "resources": self.resources + } + start = time.time() + ifcCreator = ScheduleIfcGenerator(self.file, self.output, settings) + end = time.time() + + ifcCreator.create_ifc() + end2 = time.time() + print("Parsing time is", end - start) + print("IFC Creation took", end2 - end) + print("Overall Time", end2 - start) + # self.create_ifc() + + def parse_pp(self): + + project = self.get_json("PROJECT_SUMMARY")[0] + self.project["Name"] = project["SHORT_NAME"] + self.parse_calendar_pp() + self.parse_wbs_pp() + self.parse_activity_pp() + self.parse_relationship_pp(project) + + def parse_calendar_pp(self): + calendars = self.get_json("CALENDAR") + wp_data = self.get_json("WORK_PATTERN") + + for calendar in calendars: + calendar_id = calendar["ID"] + calendar_wp = calendar["DOMINANT_WORK_PATTERN"] + wp_data = self.get_json_with_filter("WORK_PATTERN", "ID", calendar_wp) + print("wp_data", wp_data[0]['SHIFTS']) + wp = AstaCalendarWorkPattern(wp_data[0]['SHIFTS']) + print(wp.dict_wp) + exceptions = {} + timex = [] + for times in wp.dict_wp: + work_times = timedelta(hours=0) + for daily in times['WorkTimes']: + fin = daily['Finish'] + strt = daily['Start'] + work_times += timedelta(hours=fin.hour, minutes=fin.minute) - timedelta(hours=strt.hour, minutes=strt.minute) + timex.append(work_times.total_seconds() /(60*60)) + + self.calendars[calendar_id] = { + "Name": calendar["NAME"], + "Type": "NOTDEFINED", + "HoursPerDay": max(timex), + "StandardWorkWeek": wp.dict_wp, + "HolidayOrExceptions": exceptions, + } + #print(self.calendars[calendar_id]) + + def parse_wbs_pp(self): + bars = self.get_json("BAR") + extended_tasks = self.get_json("EXPANDED_TASK") + + for bar in bars: + self.wbs[bar["ID"]] = { + "Name": bar["NAME"], + "Code": bar["ID"], + "ParentObjectId": bar["EXPANDED_TASK"] if bar["EXPANDED_TASK"]> 0 else None, + "ifc": None, + "rel": None, + "activities": [], + } + for bar in extended_tasks: + self.wbs[bar["ID"]] = { + "Name": bar["NAME"], + "Code": bar["ID"], + "ParentObjectId": bar["BAR"] if bar["BAR"]> 0 else None, + "ifc": None, + "rel": None, + "activities": [], + } + #print(self.wbs) + + def parse_activity_pp(self): + activities = self.get_json("TASK") + milestones = self.get_json("MILESTONE") + for activity in activities: + activity_type = "TASK" + activity_id = activity["ID"] + wbs_id = activity["BAR"] + if wbs_id: + self.wbs[wbs_id]["activities"].append(activity_id) + else: + self.root_activites.append(activity_id) + self.activities[activity_id] = { + "Name": activity["NAME"], + "Identification": activity["ID"], + "StartDate": datetime.datetime.fromisoformat(activity["LINKABLE_START"]), + "FinishDate": datetime.datetime.fromisoformat(activity["LINKABLE_FINISH"]), + "PlannedDuration": float(activity["PLANNED_DURATION"].split(",")[-2].replace("<","").replace(">","") ), + "Status": "PLANNED", + "CalendarObjectId": activity["CALENDAR"], + "ifc": None, + } + + for activity in milestones: + activity_type = "MILESTONE" + activity_id = activity["ID"] + wbs_id = activity["BAR"] + if wbs_id: + self.wbs[wbs_id]["activities"].append(activity_id) + else: + self.root_activites.append(activity_id) + self.activities[activity_id] = { + "Name": activity["NAME"], + "Identification": activity["ID"], + "StartDate": datetime.datetime.fromisoformat(activity["LINKABLE_START"]), + "FinishDate": datetime.datetime.fromisoformat(activity["LINKABLE_FINISH"]), + "PlannedDuration": 0.0, + "Status": "PLANNED", + "CalendarObjectId": activity["CALENDAR"], + "ifc": None, + } + #print("***ACTIVITIES", self.activities[activity_id]) + + def parse_relationship_pp(self, project): + relations = self.get_json('LINK') + for relationship in relations: + predecessor = relationship['START_TASK'] + successor = relationship['END_TASK'] + if predecessor not in self.activities or successor not in self.activities: + print("!!!!!!!!!!!!!!!!!ERROR!!!!!!!!!!!!!!!!") + print(predecessor, successor) + continue + self.relationships[relationship["ID"]] = { + "PredecessorActivity": predecessor, + "SuccessorActivity": successor, + "Type": self.relationship_map[relationship['LINK_KIND']], + "Lag": float(relationship['END_LAG_TIME'].split(",")[-2].replace("<","").replace(">","")), + } diff --git a/src/ifc4d/ifc4d/wpattern.py b/src/ifc4d/ifc4d/wpattern.py new file mode 100644 index 0000000000..dc47884335 --- /dev/null +++ b/src/ifc4d/ifc4d/wpattern.py @@ -0,0 +1,61 @@ +""" +This class parses the calendar work pattern and retruns a list +The list returned has a key DayOfWeek which takes a value Sunday to Saturday +for each day as a key there is a list of working times with the format +{"Start": datetime.time, "Finish": datetime.time} + +""" + +import re +from datetime import time, datetime + + + +class AstaCalendarWorkPattern: + def get_keys(self, s): + regex = r"\<(\".+?\")\>(\w|\d|)+?" + + matches=re.finditer(regex, s) + matcs = [] + for matchNum, match in enumerate(matches, start=1): + matcs.append(match.group(1)) + return matcs + + + def get_values(self, s): + rx2 = r"<\"[^<>]+\">" + data = re.split('<[^<>]+>', s) + return data + + def __init__(self, string): + self.string = string + self.Days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + self.keys = self.get_keys(string) + self.values = self.get_values(string) + self.dict_wp = {} + self.dict_wp = [] + for d, m in zip(self.values[1:], self.keys): + splt_data = d.strip().split(",") + workhours = [] + if len(splt_data)>15: + print(splt_data) + st1_1 = datetime.strptime(splt_data[6], "%H%M%S") + st1_2 = datetime.strptime(splt_data[7], "%H%M%S") + st = {"Start":time(st1_1.hour, st1_1.minute), "Finish": time(st1_2.hour, st1_2.minute), "ifc": None} + workhours.append(st) + st2_1 = datetime.strptime(splt_data[12], "%H%M%S") + st2_2 = datetime.strptime(splt_data[13], "%H%M%S") + st2 = {"Start":time(st2_1.hour, st2_1.minute), "Finish": time(st2_2.hour, st2_2.minute), "ifc": None} + workhours.append(st2) + + self.dict_wp.append({'DayOfWeek': m.replace("\"",""), + 'WorkTimes': workhours, "ifc": None}) + + for day in self.Days: + if not len(list(filter(lambda d: d['DayOfWeek']== day , self.dict_wp))) > 0: + print("MISSING", day) + self.dict_wp.append({'DayOfWeek': day, + 'WorkTimes': [], "ifc": None}) + print("final", self.dict_wp) + +