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.BIMTaskTreeProperties,
prop.WorkCalendar,
prop.RecurrenceComponent,
prop.BIMWorkCalendarProperties,
ui.BIM_PT_work_plans,
ui.BIM_PT_work_schedules,
@@ -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"}
@@ -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"),
@@ -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:
@@ -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":