From e5cf0cde2d0be662908223c224c198e0975238e9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 29 Apr 2021 17:52:45 +1000 Subject: [PATCH] Improve time and date support (don't cast to datetime) and support all recurrence types and time periods in work schedules --- .../bim/module/sequence/__init__.py | 1 + .../bim/module/sequence/operator.py | 119 +++++++++++++++--- .../blenderbim/bim/module/sequence/prop.py | 20 +-- .../blenderbim/bim/module/sequence/ui.py | 58 ++++++--- .../ifcopenshell/util/date.py | 33 +++-- 5 files changed, 179 insertions(+), 52 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 45f1f6b15b..477f6239a3 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -58,6 +58,7 @@ classes = ( prop.BIMWorkScheduleProperties, prop.BIMTaskTreeProperties, prop.WorkCalendar, + prop.RecurrenceComponent, prop.BIMWorkCalendarProperties, ui.BIM_PT_work_plans, ui.BIM_PT_work_schedules, diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 6cb69430d0..30ddabd08f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -5,6 +5,7 @@ import time import pystache import webbrowser import ifcopenshell.api +import ifcopenshell.util.date from datetime import datetime from dateutil import parser from blenderbim.bim.ifc import IfcStore @@ -803,9 +804,9 @@ class EnableEditingWorkCalendar(bpy.types.Operator): work_calendar: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMWorkCalendarProperties - while len(props.work_calendar_attributes) > 0: - props.work_calendar_attributes.remove(0) + self.props = context.scene.BIMWorkCalendarProperties + while len(self.props.work_calendar_attributes) > 0: + self.props.work_calendar_attributes.remove(0) data = Data.work_calendars[self.work_calendar] @@ -813,7 +814,7 @@ class EnableEditingWorkCalendar(bpy.types.Operator): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue - new = props.work_calendar_attributes.add() + new = self.props.work_calendar_attributes.add() new.name = attribute.name() new.is_null = data[attribute.name()] is None new.is_optional = attribute.optional() @@ -824,8 +825,8 @@ class EnableEditingWorkCalendar(bpy.types.Operator): new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] - props.active_work_calendar_id = self.work_calendar - props.is_editing = "ATTRIBUTES" + self.props.active_work_calendar_id = self.work_calendar + self.props.is_editing = "ATTRIBUTES" return {"FINISHED"} @@ -894,9 +895,9 @@ class EnableEditingWorkTime(bpy.types.Operator): work_time: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMWorkCalendarProperties - while len(props.work_time_attributes) > 0: - props.work_time_attributes.remove(0) + self.props = context.scene.BIMWorkCalendarProperties + while len(self.props.work_time_attributes) > 0: + self.props.work_time_attributes.remove(0) data = Data.work_times[self.work_time] @@ -904,7 +905,7 @@ class EnableEditingWorkTime(bpy.types.Operator): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if data_type == "entity": continue - new = props.work_time_attributes.add() + new = self.props.work_time_attributes.add() new.name = attribute.name() new.is_null = data[attribute.name()] is None new.is_optional = attribute.optional() @@ -917,9 +918,51 @@ class EnableEditingWorkTime(bpy.types.Operator): new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) if data[attribute.name()]: new.enum_value = data[attribute.name()] - props.active_work_time_id = self.work_time + + self.initialise_recurrence_components() + self.load_recurrence_pattern_data(data) + self.props.active_work_time_id = self.work_time return {"FINISHED"} + def initialise_recurrence_components(self): + if len(self.props.day_components) == 0: + for i in range(0, 31): + new = self.props.day_components.add() + new.name = str(i + 1) + if len(self.props.weekday_components) == 0: + for d in ["M", "T", "W", "T", "F", "S", "S"]: + new = self.props.weekday_components.add() + new.name = d + if len(self.props.month_components) == 0: + for m in ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]: + new = self.props.month_components.add() + new.name = m + + def load_recurrence_pattern_data(self, work_time): + self.props.position = 0 + self.props.interval = 0 + self.props.occurrences = 0 + self.props.start_time = "" + self.props.end_time = "" + for component in self.props.day_components: + component.is_specified = False + for component in self.props.weekday_components: + component.is_specified = False + for component in self.props.month_components: + component.is_specified = False + if not work_time["RecurrencePattern"]: + return + recurrence_pattern = Data.recurrence_patterns[work_time["RecurrencePattern"]] + for attribute in ["Position", "Interval", "Occurrences"]: + if recurrence_pattern[attribute]: + setattr(self.props, attribute.lower(), recurrence_pattern[attribute]) + for component in recurrence_pattern["DayComponent"] or []: + self.props.day_components[component - 1].is_specified = True + for component in recurrence_pattern["WeekdayComponent"] or []: + self.props.weekday_components[component - 1].is_specified = True + for component in recurrence_pattern["MonthComponent"] or []: + self.props.month_components[component - 1].is_specified = True + class DisableEditingWorkTime(bpy.types.Operator): bl_idname = "bim.disable_editing_work_time" @@ -935,9 +978,9 @@ class EditWorkTime(bpy.types.Operator): bl_label = "Edit Work Time" def execute(self, context): - props = context.scene.BIMWorkCalendarProperties + self.props = context.scene.BIMWorkCalendarProperties attributes = {} - for attribute in props.work_time_attributes: + for attribute in self.props.work_time_attributes: if attribute.is_null: attributes[attribute.name] = None else: @@ -949,12 +992,49 @@ class EditWorkTime(bpy.types.Operator): ifcopenshell.api.run( "sequence.edit_work_time", self.file, - **{"work_time": self.file.by_id(props.active_work_time_id), "attributes": attributes}, + **{"work_time": self.file.by_id(self.props.active_work_time_id), "attributes": attributes}, ) + + work_time = Data.work_times[self.props.active_work_time_id] + if work_time["RecurrencePattern"]: + self.edit_recurrence_pattern(work_time["RecurrencePattern"]) + Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_work_time() return {"FINISHED"} + def edit_recurrence_pattern(self, recurrence_pattern_id): + recurrence_pattern = self.file.by_id(recurrence_pattern_id) + attributes = { + "Interval": self.props.interval if self.props.interval > 0 else None, + "Occurrences": self.props.occurrences if self.props.occurrences > 0 else None, + } + applicable_data = { + "DAILY": ["Interval", "Occurrences"], + "WEEKLY": ["WeekdayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_DAY_OF_MONTH": ["DayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_POSITION": ["WeekdayComponent", "Position", "Interval", "Occurrences"], + "BY_DAY_COUNT": ["Interval", "Occurrences"], + "BY_WEEKDAY_COUNT": ["WeekdayComponent", "Interval", "Occurrences"], + "YEARLY_BY_DAY_OF_MONTH": ["DayComponent", "MonthComponent", "Interval", "Occurrences"], + "YEARLY_BY_POSITION": ["WeekdayComponent", "MonthComponent", "Position", "Interval", "Occurrences"], + } + if "Position" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["Position"] = self.props.position if self.props.position != 0 else None + if "DayComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["DayComponent"] = [i + 1 for i, c in enumerate(self.props.day_components) if c.is_specified] + if "WeekdayComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["WeekdayComponent"] = [ + i + 1 for i, c in enumerate(self.props.weekday_components) if c.is_specified + ] + if "MonthComponent" in applicable_data[recurrence_pattern.RecurrenceType]: + attributes["MonthComponent"] = [i + 1 for i, c in enumerate(self.props.month_components) if c.is_specified] + ifcopenshell.api.run( + "sequence.edit_recurrence_pattern", + self.file, + **{"recurrence_pattern": recurrence_pattern, "attributes": attributes}, + ) + class RemoveWorkTime(bpy.types.Operator): bl_idname = "bim.remove_work_time" @@ -1009,15 +1089,22 @@ class AddTimePeriod(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMWorkCalendarProperties self.file = IfcStore.get_file() + try: + start_time = parser.parse(self.props.start_time) + end_time = parser.parse(self.props.end_time) + except: + return {"FINISHED"} ifcopenshell.api.run( "sequence.add_time_period", self.file, **{ "recurrence_pattern": self.file.by_id(self.recurrence_pattern), - "start_time": self.props.start_time, - "end_time": self.props.start_time, + "start_time": start_time, + "end_time": end_time, }, ) + self.props.start_time = "" + self.props.end_time = "" Data.load(IfcStore.get_file()) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 15a25f33d6..2d02e2236d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -168,6 +168,11 @@ class WorkCalendar(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +class RecurrenceComponent(PropertyGroup): + name: StringProperty(name="Name") + is_specified: BoolProperty(name="Is Specified") + + class BIMWorkCalendarProperties(PropertyGroup): work_calendar_attributes: CollectionProperty(name="Work Calendar Attributes", type=Attribute) work_time_attributes: CollectionProperty(name="Work Time Attributes", type=Attribute) @@ -175,15 +180,12 @@ class BIMWorkCalendarProperties(PropertyGroup): work_calendars: CollectionProperty(name="Work Calendar", type=WorkCalendar) active_work_calendar_id: IntProperty(name="Active Work Calendar Id") active_work_time_id: IntProperty(name="Active Work Time Id") - weekday_component_monday: BoolProperty(name="M") - weekday_component_tuesday: BoolProperty(name="T") - weekday_component_wednesday: BoolProperty(name="W") - weekday_component_thursday: BoolProperty(name="T") - weekday_component_friday: BoolProperty(name="F") - weekday_component_saturday: BoolProperty(name="S") - weekday_component_sunday: BoolProperty(name="S") - dummy_bool: BoolProperty(name="Active Work Calendar Id") - dummy_int: IntProperty(name="Active Work Calendar Id") + day_components: CollectionProperty(name="Day Components", type=RecurrenceComponent) + weekday_components: CollectionProperty(name="Weekday Components", type=RecurrenceComponent) + month_components: CollectionProperty(name="Month Components", type=RecurrenceComponent) + position: IntProperty(name="Position") + interval: IntProperty(name="Recurrence Interval") + occurrences: IntProperty(name="Occurs N Times") recurrence_types: EnumProperty(items=[ ("DAILY", "Daily", "e.g. Every day"), ("WEEKLY", "Weekly", "e.g. Every Friday"), diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index a395eedb1c..59fab50da7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -74,7 +74,6 @@ class BIM_PT_work_plans(Panel): op.work_schedule = int(self.props.work_schedules) - class BIM_PT_work_schedules(Panel): bl_label = "IFC Work Schedules" bl_idname = "BIM_PT_work_schedules" @@ -116,7 +115,9 @@ class BIM_PT_work_schedules(Panel): row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id else: row.operator("bim.enable_editing_tasks", text="", icon="ACTION").work_schedule = work_schedule_id - row.operator("bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL").work_schedule = work_schedule_id + row.operator( + "bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL" + ).work_schedule = work_schedule_id row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id if self.props.active_work_schedule_id == work_schedule_id: @@ -311,7 +312,11 @@ class BIM_PT_work_calendars(Panel): def draw_work_time_ui(self, work_time, time_type): row = self.layout.row(align=True) - row.label(text=work_time["Name"] or "Unnamed", icon="MESH_GRID" if time_type == "WorkingTimes" else "LIGHTPROBE_GRID") + row.label( + text=work_time["Name"] or "Unnamed", icon="MESH_GRID" if time_type == "WorkingTimes" else "LIGHTPROBE_GRID" + ) + if work_time["Start"] or work_time["Finish"]: + row.label(text="{} - {}".format(work_time["Start"] or "*", work_time["Finish"] or "*")) if self.props.active_work_time_id == work_time["id"]: row.operator("bim.edit_work_time", text="", icon="CHECKMARK") row.operator("bim.disable_editing_work_time", text="", icon="CANCEL") @@ -366,23 +371,42 @@ class BIM_PT_work_calendars(Panel): op = row.operator("bim.remove_time_period", text="", icon="X") op.time_period = time_period_id - if recurrence_pattern["RecurrenceType"] == "DAILY": - pass # No need to show any custom UI - if recurrence_pattern["RecurrenceType"] == "WEEKLY": + applicable_data = { + "DAILY": ["Interval", "Occurrences"], + "WEEKLY": ["WeekdayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_DAY_OF_MONTH": ["DayComponent", "Interval", "Occurrences"], + "MONTHLY_BY_POSITION": ["WeekdayComponent", "Position", "Interval", "Occurrences"], + "BY_DAY_COUNT": ["Interval", "Occurrences"], + "BY_WEEKDAY_COUNT": ["WeekdayComponent", "Interval", "Occurrences"], + "YEARLY_BY_DAY_OF_MONTH": ["DayComponent", "MonthComponent", "Interval", "Occurrences"], + "YEARLY_BY_POSITION": ["WeekdayComponent", "MonthComponent", "Position", "Interval", "Occurrences"], + } + + if "Position" in applicable_data[recurrence_pattern["RecurrenceType"]]: + row = box.row() + row.prop(self.props, "position") + + if "DayComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: + for i, component in enumerate(self.props.day_components): + if i % 7 == 0: + row = box.row(align=True) + row.prop(component, "is_specified", text=component.name) + + if "WeekdayComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: row = box.row(align=True) - row.prop(self.props, "weekday_component_monday", text="M") - row.prop(self.props, "weekday_component_tuesday", text="T") - row.prop(self.props, "weekday_component_wednesday", text="W") - row.prop(self.props, "weekday_component_thursday", text="T") - row.prop(self.props, "weekday_component_friday", text="F") - row.prop(self.props, "weekday_component_saturday", text="S") - row.prop(self.props, "weekday_component_sunday", text="S") + for component in self.props.weekday_components: + row.prop(component, "is_specified", text=component.name) - row = box.row(align=True) - row.prop(self.props, "dummy_int", text="Recurrence Interval") - row = box.row(align=True) - row.prop(self.props, "dummy_int", text="Occurs N Times") + if "MonthComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]: + for i, component in enumerate(self.props.month_components): + if i % 4 == 0: + row = box.row(align=True) + row.prop(component, "is_specified", text=component.name) + row = box.row() + row.prop(self.props, "interval") + row = box.row() + row.prop(self.props, "occurrences") def draw_editable_ui(self): for attribute in self.props.work_calendar_attributes: diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index d1b0ad5d7e..8d37e952ca 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -1,5 +1,5 @@ +import datetime from re import findall -from datetime import datetime def duration2dict(duration): @@ -12,12 +12,16 @@ def duration2dict(duration): def ifc2datetime(element): if isinstance(element, str) and element[0] == "P": # IfcDuration return duration2dict(element) - elif isinstance(element, str): # IfcDateTime, IfcDate - return datetime.fromisoformat(element) + elif isinstance(element, str) and element[2] == ":": # IfcTime + return datetime.time.fromisoformat(element) + elif isinstance(element, str) and ":" in element: # IfcDateTime + return datetime.datetime.fromisoformat(element) + elif isinstance(element, str): # IfcDate + return datetime.date.fromisoformat(element) elif isinstance(element, int): # IfcTimeStamp - return datetime.fromtimestamp(element) + return datetime.datetime.fromtimestamp(element) elif element.is_a("IfcDateAndTime"): - return datetime( + return datetime.datetime( element.DateComponent.YearComponent, element.DateComponent.MonthComponent, element.DateComponent.DayComponent, @@ -27,7 +31,7 @@ def ifc2datetime(element): # TODO: implement TimeComponent timezone ) elif element.is_a("IfcCalendarDate"): - return datetime( + return datetime.date( element.YearComponent, element.MonthComponent, element.DayComponent, @@ -36,15 +40,24 @@ def ifc2datetime(element): def datetime2ifc(dt, ifc_type): if isinstance(dt, str): - dt = datetime.fromisoformat(dt) + dt = datetime.datetime.fromisoformat(dt) if ifc_type == "IfcTimeStamp": return int(dt.timestamp()) elif ifc_type == "IfcDateTime": - return dt.isoformat() + if isinstance(dt, datetime.datetime): + return dt.isoformat() + elif isinstance(dt, datetime.date): + return datetime.datetime.combine(dt, datetime.datetime.min.time()).isoformat() elif ifc_type == "IfcDate": - return dt.date().isoformat() + if isinstance(dt, datetime.datetime): + return dt.date().isoformat() + elif isinstance(dt, datetime.date): + return dt.isoformat() elif ifc_type == "IfcTime": - return dt.time().isoformat() + if isinstance(dt, datetime.datetime): + return dt.time().isoformat() + elif isinstance(dt, datetime.time): + return dt.isoformat() elif ifc_type == "IfcCalendarDate": return {"DayComponent": dt.day, "MonthComponent": dt.month, "YearComponent": dt.year} elif ifc_type == "IfcLocalTime":