Improve time and date support (don't cast to datetime) and support all recurrence types and time periods in work schedules

This commit is contained in:
Dion Moult
2021-04-29 17:52:45 +10:00
parent 78e4478242
commit e5cf0cde2d
5 changed files with 179 additions and 52 deletions
@@ -58,6 +58,7 @@ classes = (
prop.BIMWorkScheduleProperties, prop.BIMWorkScheduleProperties,
prop.BIMTaskTreeProperties, prop.BIMTaskTreeProperties,
prop.WorkCalendar, prop.WorkCalendar,
prop.RecurrenceComponent,
prop.BIMWorkCalendarProperties, prop.BIMWorkCalendarProperties,
ui.BIM_PT_work_plans, ui.BIM_PT_work_plans,
ui.BIM_PT_work_schedules, ui.BIM_PT_work_schedules,
@@ -5,6 +5,7 @@ import time
import pystache import pystache
import webbrowser import webbrowser
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.date
from datetime import datetime from datetime import datetime
from dateutil import parser from dateutil import parser
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
@@ -803,9 +804,9 @@ class EnableEditingWorkCalendar(bpy.types.Operator):
work_calendar: bpy.props.IntProperty() work_calendar: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
props = context.scene.BIMWorkCalendarProperties self.props = context.scene.BIMWorkCalendarProperties
while len(props.work_calendar_attributes) > 0: while len(self.props.work_calendar_attributes) > 0:
props.work_calendar_attributes.remove(0) self.props.work_calendar_attributes.remove(0)
data = Data.work_calendars[self.work_calendar] 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) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
if data_type == "entity": if data_type == "entity":
continue continue
new = props.work_calendar_attributes.add() new = self.props.work_calendar_attributes.add()
new.name = attribute.name() new.name = attribute.name()
new.is_null = data[attribute.name()] is None new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional() 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)) new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]: if data[attribute.name()]:
new.enum_value = data[attribute.name()] new.enum_value = data[attribute.name()]
props.active_work_calendar_id = self.work_calendar self.props.active_work_calendar_id = self.work_calendar
props.is_editing = "ATTRIBUTES" self.props.is_editing = "ATTRIBUTES"
return {"FINISHED"} return {"FINISHED"}
@@ -894,9 +895,9 @@ class EnableEditingWorkTime(bpy.types.Operator):
work_time: bpy.props.IntProperty() work_time: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
props = context.scene.BIMWorkCalendarProperties self.props = context.scene.BIMWorkCalendarProperties
while len(props.work_time_attributes) > 0: while len(self.props.work_time_attributes) > 0:
props.work_time_attributes.remove(0) self.props.work_time_attributes.remove(0)
data = Data.work_times[self.work_time] 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) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
if data_type == "entity": if data_type == "entity":
continue continue
new = props.work_time_attributes.add() new = self.props.work_time_attributes.add()
new.name = attribute.name() new.name = attribute.name()
new.is_null = data[attribute.name()] is None new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional() 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)) new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]: if data[attribute.name()]:
new.enum_value = 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"} 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): class DisableEditingWorkTime(bpy.types.Operator):
bl_idname = "bim.disable_editing_work_time" bl_idname = "bim.disable_editing_work_time"
@@ -935,9 +978,9 @@ class EditWorkTime(bpy.types.Operator):
bl_label = "Edit Work Time" bl_label = "Edit Work Time"
def execute(self, context): def execute(self, context):
props = context.scene.BIMWorkCalendarProperties self.props = context.scene.BIMWorkCalendarProperties
attributes = {} attributes = {}
for attribute in props.work_time_attributes: for attribute in self.props.work_time_attributes:
if attribute.is_null: if attribute.is_null:
attributes[attribute.name] = None attributes[attribute.name] = None
else: else:
@@ -949,12 +992,49 @@ class EditWorkTime(bpy.types.Operator):
ifcopenshell.api.run( ifcopenshell.api.run(
"sequence.edit_work_time", "sequence.edit_work_time",
self.file, 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()) Data.load(IfcStore.get_file())
bpy.ops.bim.disable_editing_work_time() bpy.ops.bim.disable_editing_work_time()
return {"FINISHED"} 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): class RemoveWorkTime(bpy.types.Operator):
bl_idname = "bim.remove_work_time" bl_idname = "bim.remove_work_time"
@@ -1009,15 +1089,22 @@ class AddTimePeriod(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.props = context.scene.BIMWorkCalendarProperties self.props = context.scene.BIMWorkCalendarProperties
self.file = IfcStore.get_file() 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( ifcopenshell.api.run(
"sequence.add_time_period", "sequence.add_time_period",
self.file, self.file,
**{ **{
"recurrence_pattern": self.file.by_id(self.recurrence_pattern), "recurrence_pattern": self.file.by_id(self.recurrence_pattern),
"start_time": self.props.start_time, "start_time": start_time,
"end_time": self.props.start_time, "end_time": end_time,
}, },
) )
self.props.start_time = ""
self.props.end_time = ""
Data.load(IfcStore.get_file()) Data.load(IfcStore.get_file())
return {"FINISHED"} return {"FINISHED"}
@@ -168,6 +168,11 @@ class WorkCalendar(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_definition_id: IntProperty(name="IFC Definition ID")
class RecurrenceComponent(PropertyGroup):
name: StringProperty(name="Name")
is_specified: BoolProperty(name="Is Specified")
class BIMWorkCalendarProperties(PropertyGroup): class BIMWorkCalendarProperties(PropertyGroup):
work_calendar_attributes: CollectionProperty(name="Work Calendar Attributes", type=Attribute) work_calendar_attributes: CollectionProperty(name="Work Calendar Attributes", type=Attribute)
work_time_attributes: CollectionProperty(name="Work Time 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) work_calendars: CollectionProperty(name="Work Calendar", type=WorkCalendar)
active_work_calendar_id: IntProperty(name="Active Work Calendar Id") active_work_calendar_id: IntProperty(name="Active Work Calendar Id")
active_work_time_id: IntProperty(name="Active Work Time Id") active_work_time_id: IntProperty(name="Active Work Time Id")
weekday_component_monday: BoolProperty(name="M") day_components: CollectionProperty(name="Day Components", type=RecurrenceComponent)
weekday_component_tuesday: BoolProperty(name="T") weekday_components: CollectionProperty(name="Weekday Components", type=RecurrenceComponent)
weekday_component_wednesday: BoolProperty(name="W") month_components: CollectionProperty(name="Month Components", type=RecurrenceComponent)
weekday_component_thursday: BoolProperty(name="T") position: IntProperty(name="Position")
weekday_component_friday: BoolProperty(name="F") interval: IntProperty(name="Recurrence Interval")
weekday_component_saturday: BoolProperty(name="S") occurrences: IntProperty(name="Occurs N Times")
weekday_component_sunday: BoolProperty(name="S")
dummy_bool: BoolProperty(name="Active Work Calendar Id")
dummy_int: IntProperty(name="Active Work Calendar Id")
recurrence_types: EnumProperty(items=[ recurrence_types: EnumProperty(items=[
("DAILY", "Daily", "e.g. Every day"), ("DAILY", "Daily", "e.g. Every day"),
("WEEKLY", "Weekly", "e.g. Every Friday"), ("WEEKLY", "Weekly", "e.g. Every Friday"),
@@ -74,7 +74,6 @@ class BIM_PT_work_plans(Panel):
op.work_schedule = int(self.props.work_schedules) op.work_schedule = int(self.props.work_schedules)
class BIM_PT_work_schedules(Panel): class BIM_PT_work_schedules(Panel):
bl_label = "IFC Work Schedules" bl_label = "IFC Work Schedules"
bl_idname = "BIM_PT_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 row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
else: else:
row.operator("bim.enable_editing_tasks", text="", icon="ACTION").work_schedule = work_schedule_id 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 row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
if self.props.active_work_schedule_id == 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): def draw_work_time_ui(self, work_time, time_type):
row = self.layout.row(align=True) 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"]: if self.props.active_work_time_id == work_time["id"]:
row.operator("bim.edit_work_time", text="", icon="CHECKMARK") row.operator("bim.edit_work_time", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_work_time", text="", icon="CANCEL") 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 = row.operator("bim.remove_time_period", text="", icon="X")
op.time_period = time_period_id op.time_period = time_period_id
if recurrence_pattern["RecurrenceType"] == "DAILY": applicable_data = {
pass # No need to show any custom UI "DAILY": ["Interval", "Occurrences"],
if recurrence_pattern["RecurrenceType"] == "WEEKLY": "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 = box.row(align=True)
row.prop(self.props, "weekday_component_monday", text="M") for component in self.props.weekday_components:
row.prop(self.props, "weekday_component_tuesday", text="T") row.prop(component, "is_specified", text=component.name)
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")
row = box.row(align=True) if "MonthComponent" in applicable_data[recurrence_pattern["RecurrenceType"]]:
row.prop(self.props, "dummy_int", text="Recurrence Interval") for i, component in enumerate(self.props.month_components):
row = box.row(align=True) if i % 4 == 0:
row.prop(self.props, "dummy_int", text="Occurs N Times") 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): def draw_editable_ui(self):
for attribute in self.props.work_calendar_attributes: for attribute in self.props.work_calendar_attributes:
@@ -1,5 +1,5 @@
import datetime
from re import findall from re import findall
from datetime import datetime
def duration2dict(duration): def duration2dict(duration):
@@ -12,12 +12,16 @@ def duration2dict(duration):
def ifc2datetime(element): def ifc2datetime(element):
if isinstance(element, str) and element[0] == "P": # IfcDuration if isinstance(element, str) and element[0] == "P": # IfcDuration
return duration2dict(element) return duration2dict(element)
elif isinstance(element, str): # IfcDateTime, IfcDate elif isinstance(element, str) and element[2] == ":": # IfcTime
return datetime.fromisoformat(element) 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 elif isinstance(element, int): # IfcTimeStamp
return datetime.fromtimestamp(element) return datetime.datetime.fromtimestamp(element)
elif element.is_a("IfcDateAndTime"): elif element.is_a("IfcDateAndTime"):
return datetime( return datetime.datetime(
element.DateComponent.YearComponent, element.DateComponent.YearComponent,
element.DateComponent.MonthComponent, element.DateComponent.MonthComponent,
element.DateComponent.DayComponent, element.DateComponent.DayComponent,
@@ -27,7 +31,7 @@ def ifc2datetime(element):
# TODO: implement TimeComponent timezone # TODO: implement TimeComponent timezone
) )
elif element.is_a("IfcCalendarDate"): elif element.is_a("IfcCalendarDate"):
return datetime( return datetime.date(
element.YearComponent, element.YearComponent,
element.MonthComponent, element.MonthComponent,
element.DayComponent, element.DayComponent,
@@ -36,15 +40,24 @@ def ifc2datetime(element):
def datetime2ifc(dt, ifc_type): def datetime2ifc(dt, ifc_type):
if isinstance(dt, str): if isinstance(dt, str):
dt = datetime.fromisoformat(dt) dt = datetime.datetime.fromisoformat(dt)
if ifc_type == "IfcTimeStamp": if ifc_type == "IfcTimeStamp":
return int(dt.timestamp()) return int(dt.timestamp())
elif ifc_type == "IfcDateTime": 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": 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": 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": elif ifc_type == "IfcCalendarDate":
return {"DayComponent": dt.day, "MonthComponent": dt.month, "YearComponent": dt.year} return {"DayComponent": dt.day, "MonthComponent": dt.month, "YearComponent": dt.year}
elif ifc_type == "IfcLocalTime": elif ifc_type == "IfcLocalTime":