1use ifc_model::{Entity, EntityId, Transaction, Value};
26
27use crate::calendar::recurrence_slot;
28use crate::calendar::{work_calendar_slot, work_time_slot};
29use crate::error::ScheduleAuthoringError;
30use crate::event::{event_slot, event_time_slot};
31use crate::query::{assigns_slot, nests_slot};
32use crate::schedule::work_control_slot as control_slot;
33use crate::schedule::WorkControlKind;
34use crate::sequence::lag_slot;
35use crate::sequence::relation::slot as sequence_slot;
36use crate::task::definition::task_slot;
37
38pub type ScheduleAuthoringResult<T> = Result<T, ScheduleAuthoringError>;
40
41#[derive(Debug, Clone, Copy, Default)]
43pub struct TaskDraft<'a> {
44 pub global_id: &'a str,
46 pub name: Option<&'a str>,
48 pub description: Option<&'a str>,
50 pub identification: Option<&'a str>,
52 pub long_description: Option<&'a str>,
54 pub status: Option<&'a str>,
56 pub work_method: Option<&'a str>,
58 pub is_milestone: bool,
60 pub priority: Option<i64>,
62 pub task_time: Option<EntityId>,
64 pub predefined_type: Option<&'a str>,
66}
67
68pub fn create_task(
79 tx: &mut Transaction,
80 draft: TaskDraft<'_>,
81) -> ScheduleAuthoringResult<EntityId> {
82 checks::task(&draft)?;
83 let mut attributes = vec![Value::Null; task_slot::PREDEFINED_TYPE + 1];
84 attributes[task_slot::GLOBAL_ID] = Value::Text(draft.global_id.into());
85 attributes[task_slot::NAME] = optional_text(draft.name);
86 attributes[task_slot::DESCRIPTION] = optional_text(draft.description);
87 attributes[task_slot::IDENTIFICATION] = optional_text(draft.identification);
88 attributes[task_slot::LONG_DESCRIPTION] = optional_text(draft.long_description);
89 attributes[task_slot::STATUS] = optional_text(draft.status);
90 attributes[task_slot::WORK_METHOD] = optional_text(draft.work_method);
91 attributes[task_slot::IS_MILESTONE] = Value::Bool(draft.is_milestone);
92 attributes[task_slot::PRIORITY] = draft.priority.map_or(Value::Null, Value::Integer);
93 attributes[task_slot::TASK_TIME] = draft.task_time.map_or(Value::Null, Value::Ref);
94 attributes[task_slot::PREDEFINED_TYPE] = draft
95 .predefined_type
96 .map_or(Value::Null, |t| Value::Enum(t.into()));
97 Ok(tx.create(Entity::new("IFCTASK", attributes)))
98}
99
100pub(crate) fn optional_text(value: Option<&str>) -> Value {
101 value.map_or(Value::Null, |text| Value::Text(text.into()))
102}
103pub fn create_sequence(
115 tx: &mut Transaction,
116 global_id: &str,
117 predecessor: EntityId,
118 successor: EntityId,
119 sequence_type: Option<&str>,
120 time_lag: Option<EntityId>,
121) -> ScheduleAuthoringResult<EntityId> {
122 checks::sequence(global_id, predecessor, successor)?;
123 let mut attributes = vec![Value::Null; sequence_slot::SEQUENCE_TYPE + 2];
124 attributes[0] = Value::Text(global_id.into());
125 attributes[sequence_slot::RELATING] = Value::Ref(predecessor);
126 attributes[sequence_slot::RELATED] = Value::Ref(successor);
127 attributes[sequence_slot::TIME_LAG] = time_lag.map_or(Value::Null, Value::Ref);
128 attributes[sequence_slot::SEQUENCE_TYPE] =
129 sequence_type.map_or(Value::Null, |t| Value::Enum(t.into()));
130 Ok(tx.create(Entity::new("IFCRELSEQUENCE", attributes)))
131}
132
133#[derive(Debug, Clone, Copy, Default)]
140pub struct WorkControlDraft<'a> {
141 pub global_id: &'a str,
143 pub name: Option<&'a str>,
145 pub description: Option<&'a str>,
147 pub identification: Option<&'a str>,
149 pub creation_date: &'a str,
151 pub purpose: Option<&'a str>,
153 pub duration: Option<&'a str>,
155 pub total_float: Option<&'a str>,
157 pub start_time: &'a str,
159 pub finish_time: Option<&'a str>,
161 pub predefined_type: Option<&'a str>,
163}
164
165pub fn create_work_control(
182 tx: &mut Transaction,
183 kind: WorkControlKind,
184 draft: WorkControlDraft<'_>,
185) -> ScheduleAuthoringResult<EntityId> {
186 let type_name = checks::work_control(kind, &draft)?;
187 let mut attributes = vec![Value::Null; control_slot::PREDEFINED_TYPE + 1];
188 attributes[control_slot::GLOBAL_ID] = Value::Text(draft.global_id.into());
189 attributes[control_slot::NAME] = optional_text(draft.name);
190 attributes[control_slot::DESCRIPTION] = optional_text(draft.description);
191 attributes[control_slot::IDENTIFICATION] = optional_text(draft.identification);
192 attributes[control_slot::CREATION_DATE] = Value::Text(draft.creation_date.into());
193 attributes[control_slot::PURPOSE] = optional_text(draft.purpose);
194 attributes[control_slot::DURATION] = optional_text(draft.duration);
195 attributes[control_slot::TOTAL_FLOAT] = optional_text(draft.total_float);
196 attributes[control_slot::START_TIME] = Value::Text(draft.start_time.into());
197 attributes[control_slot::FINISH_TIME] = optional_text(draft.finish_time);
198 attributes[control_slot::PREDEFINED_TYPE] = draft
199 .predefined_type
200 .map_or(Value::Null, |t| Value::Enum(t.into()));
201 Ok(tx.create(Entity::new(type_name, attributes)))
202}
203
204pub fn assign_tasks_to_control(
220 tx: &mut Transaction,
221 global_id: &str,
222 control: EntityId,
223 tasks: &[EntityId],
224) -> ScheduleAuthoringResult<EntityId> {
225 checks::assignment(global_id, tasks)?;
226 let mut attributes = vec![Value::Null; assigns_slot::RELATING + 1];
227 attributes[assigns_slot::GLOBAL_ID] = Value::Text(global_id.into());
228 attributes[assigns_slot::RELATED] =
229 Value::List(tasks.iter().copied().map(Value::Ref).collect());
230 attributes[assigns_slot::RELATING] = Value::Ref(control);
231 Ok(tx.create(Entity::new("IFCRELASSIGNSTOCONTROL", attributes)))
232}
233
234pub fn nest_tasks(
252 tx: &mut Transaction,
253 global_id: &str,
254 parent: EntityId,
255 children: &[EntityId],
256) -> ScheduleAuthoringResult<EntityId> {
257 checks::nesting(global_id, parent, children)?;
258 let mut attributes = vec![Value::Null; nests_slot::RELATED + 1];
259 attributes[nests_slot::GLOBAL_ID] = Value::Text(global_id.into());
260 attributes[nests_slot::RELATING] = Value::Ref(parent);
261 attributes[nests_slot::RELATED] =
262 Value::List(children.iter().copied().map(Value::Ref).collect());
263 Ok(tx.create(Entity::new("IFCRELNESTS", attributes)))
264}
265
266pub fn create_work_time(
277 tx: &mut Transaction,
278 name: Option<&str>,
279 recurrence: Option<EntityId>,
280 start: Option<&str>,
281 finish: Option<&str>,
282) -> ScheduleAuthoringResult<EntityId> {
283 if name.is_some_and(|value| value.trim().is_empty()) {
284 return Err(ScheduleAuthoringError::InvalidValue {
285 entity: "IFCWORKTIME",
286 attribute: "Name",
287 expected: "a non-empty name when one is given",
288 });
289 }
290 let mut attributes = vec![Value::Null; work_time_slot::FINISH + 1];
291 attributes[work_time_slot::NAME] = optional_text(name);
292 attributes[work_time_slot::RECURRENCE_PATTERN] = recurrence.map_or(Value::Null, Value::Ref);
293 attributes[work_time_slot::START] = optional_text(start);
294 attributes[work_time_slot::FINISH] = optional_text(finish);
295 Ok(tx.create(Entity::new("IFCWORKTIME", attributes)))
296}
297
298pub fn create_work_calendar(
315 tx: &mut Transaction,
316 global_id: &str,
317 name: Option<&str>,
318 working_times: &[EntityId],
319 exception_times: &[EntityId],
320 predefined_type: Option<&str>,
321) -> ScheduleAuthoringResult<EntityId> {
322 checks::calendar(global_id, working_times, exception_times)?;
323 let mut attributes = vec![Value::Null; work_calendar_slot::PREDEFINED_TYPE + 1];
324 attributes[work_calendar_slot::GLOBAL_ID] = Value::Text(global_id.into());
325 attributes[work_calendar_slot::NAME] = optional_text(name);
326 attributes[work_calendar_slot::WORKING_TIMES] = reference_list(working_times);
327 attributes[work_calendar_slot::EXCEPTION_TIMES] = reference_list(exception_times);
328 attributes[work_calendar_slot::PREDEFINED_TYPE] =
329 predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
330 Ok(tx.create(Entity::new("IFCWORKCALENDAR", attributes)))
331}
332
333pub(super) fn reference_list(ids: &[EntityId]) -> Value {
338 if ids.is_empty() {
339 return Value::Null;
340 }
341 Value::List(ids.iter().copied().map(Value::Ref).collect())
342}
343
344#[derive(Debug, Clone, Copy, Default)]
350pub struct EventDraft<'a> {
351 pub global_id: &'a str,
353 pub name: Option<&'a str>,
355 pub object_type: Option<&'a str>,
358 pub identification: Option<&'a str>,
360 pub long_description: Option<&'a str>,
362 pub predefined_type: Option<&'a str>,
364 pub trigger_type: Option<&'a str>,
366 pub user_defined_trigger_type: Option<&'a str>,
369 pub occurence_time: Option<EntityId>,
371}
372
373pub fn create_event(
386 tx: &mut Transaction,
387 draft: EventDraft<'_>,
388) -> ScheduleAuthoringResult<EntityId> {
389 checks::event(&draft)?;
390 let mut attributes = vec![Value::Null; event_slot::EVENT_OCCURENCE_TIME + 1];
391 attributes[event_slot::GLOBAL_ID] = Value::Text(draft.global_id.into());
392 attributes[event_slot::NAME] = optional_text(draft.name);
393 attributes[event_slot::OBJECT_TYPE] = optional_text(draft.object_type);
394 attributes[event_slot::IDENTIFICATION] = optional_text(draft.identification);
395 attributes[event_slot::LONG_DESCRIPTION] = optional_text(draft.long_description);
396 attributes[event_slot::PREDEFINED_TYPE] = optional_enum(draft.predefined_type);
397 attributes[event_slot::EVENT_TRIGGER_TYPE] = optional_enum(draft.trigger_type);
398 attributes[event_slot::USER_DEFINED_EVENT_TRIGGER_TYPE] =
399 optional_text(draft.user_defined_trigger_type);
400 attributes[event_slot::EVENT_OCCURENCE_TIME] =
401 draft.occurence_time.map_or(Value::Null, Value::Ref);
402 Ok(tx.create(Entity::new("IFCEVENT", attributes)))
403}
404
405pub(super) fn is_user_defined(value: Option<&str>) -> bool {
407 value.is_some_and(|v| v.eq_ignore_ascii_case("USERDEFINED"))
408}
409
410pub(super) fn blank(value: Option<&str>) -> bool {
415 value.is_none_or(|v| v.trim().is_empty())
416}
417
418pub(super) fn optional_enum(value: Option<&str>) -> Value {
420 value.map_or(Value::Null, |v| Value::Enum(v.into()))
421}
422
423#[derive(Debug, Clone, Copy, Default)]
428pub struct EventTimeDraft<'a> {
429 pub name: Option<&'a str>,
431 pub actual: Option<&'a str>,
433 pub early: Option<&'a str>,
435 pub late: Option<&'a str>,
437 pub schedule: Option<&'a str>,
439}
440
441pub fn create_event_time(
447 tx: &mut Transaction,
448 draft: EventTimeDraft<'_>,
449) -> ScheduleAuthoringResult<EntityId> {
450 let mut attributes = vec![Value::Null; event_time_slot::SCHEDULE_DATE + 1];
451 attributes[event_time_slot::NAME] = optional_text(draft.name);
452 attributes[event_time_slot::ACTUAL_DATE] = optional_text(draft.actual);
453 attributes[event_time_slot::EARLY_DATE] = optional_text(draft.early);
454 attributes[event_time_slot::LATE_DATE] = optional_text(draft.late);
455 attributes[event_time_slot::SCHEDULE_DATE] = optional_text(draft.schedule);
456 Ok(tx.create(Entity::new("IFCEVENTTIME", attributes)))
457}
458
459pub fn create_lag_time(
476 tx: &mut Transaction,
477 name: Option<&str>,
478 lag_value: Value,
479 duration_type: &str,
480) -> ScheduleAuthoringResult<EntityId> {
481 if duration_type.trim().is_empty() {
482 return Err(ScheduleAuthoringError::InvalidValue {
483 entity: "IFCLAGTIME",
484 attribute: "DurationType",
485 expected: "a non-empty IfcTaskDurationEnum value",
486 });
487 }
488 let (member, payload) = match lag_value {
490 Value::Typed { type_name, value } => (Some(type_name), *value),
491 bare => (None, bare),
492 };
493 let is = |name: &str| member.as_ref().is_none_or(|m| m.eq_ignore_ascii_case(name));
494 #[allow(clippy::cast_precision_loss)]
495 let (member, payload) = match payload {
496 Value::Text(text) if is("IFCDURATION") => ("IFCDURATION", Value::Text(text)),
497 Value::Real(ratio) if is("IFCRATIOMEASURE") => ("IFCRATIOMEASURE", Value::Real(ratio)),
498 Value::Integer(ratio) if is("IFCRATIOMEASURE") => {
499 ("IFCRATIOMEASURE", Value::Real(ratio as f64))
500 }
501 _ => {
502 return Err(ScheduleAuthoringError::InvalidValue {
503 entity: "IFCLAGTIME",
504 attribute: "LagValue",
505 expected: "a duration string or a ratio number",
506 })
507 }
508 };
509 let lag_value = Value::Typed {
510 type_name: member.into(),
511 value: Box::new(payload),
512 };
513 let mut attributes = vec![Value::Null; lag_slot::DURATION_TYPE + 1];
514 attributes[event_time_slot::NAME] = optional_text(name);
515 attributes[lag_slot::LAG_VALUE] = lag_value;
516 attributes[lag_slot::DURATION_TYPE] = Value::Enum(duration_type.into());
517 Ok(tx.create(Entity::new("IFCLAGTIME", attributes)))
518}
519
520#[derive(Debug, Clone, Default)]
527pub struct RecurrenceDraft<'a> {
528 pub recurrence_type: &'a str,
530 pub days: Vec<i64>,
532 pub weekdays: Vec<i64>,
534 pub months: Vec<i64>,
536 pub position: Option<i64>,
538 pub interval: Option<i64>,
540 pub occurrences: Option<i64>,
542}
543
544pub fn create_recurrence_pattern(
550 tx: &mut Transaction,
551 draft: &RecurrenceDraft<'_>,
552) -> ScheduleAuthoringResult<EntityId> {
553 if draft.recurrence_type.trim().is_empty() {
554 return Err(ScheduleAuthoringError::InvalidValue {
555 entity: "IFCRECURRENCEPATTERN",
556 attribute: "RecurrenceType",
557 expected: "a non-empty IfcRecurrenceTypeEnum value",
558 });
559 }
560 check_range(&draft.days, 1, 31, "DayComponent")?;
561 check_range(&draft.weekdays, 1, 7, "WeekdayComponent")?;
562 check_range(&draft.months, 1, 12, "MonthComponent")?;
563 check_positive(draft.interval, "Interval")?;
564 check_positive(draft.occurrences, "Occurrences")?;
565 let mut attributes = vec![Value::Null; recurrence_slot::OCCURRENCES + 1];
566 attributes[recurrence_slot::RECURRENCE_TYPE] = Value::Enum(draft.recurrence_type.into());
567 attributes[recurrence_slot::DAY_COMPONENT] = integer_list(&draft.days);
568 attributes[recurrence_slot::WEEKDAY_COMPONENT] = integer_list(&draft.weekdays);
569 attributes[recurrence_slot::MONTH_COMPONENT] = integer_list(&draft.months);
570 attributes[recurrence_slot::POSITION] = draft.position.map_or(Value::Null, Value::Integer);
571 attributes[recurrence_slot::INTERVAL] = draft.interval.map_or(Value::Null, Value::Integer);
572 attributes[recurrence_slot::OCCURRENCES] =
573 draft.occurrences.map_or(Value::Null, Value::Integer);
574 Ok(tx.create(Entity::new("IFCRECURRENCEPATTERN", attributes)))
575}
576
577fn check_range(
579 values: &[i64],
580 low: i64,
581 high: i64,
582 attribute: &'static str,
583) -> ScheduleAuthoringResult<()> {
584 if values.iter().any(|v| !(low..=high).contains(v)) {
585 return Err(ScheduleAuthoringError::InvalidValue {
586 entity: "IFCRECURRENCEPATTERN",
587 attribute,
588 expected: "components within the schema's 1-based range",
589 });
590 }
591 Ok(())
592}
593
594fn check_positive(value: Option<i64>, attribute: &'static str) -> ScheduleAuthoringResult<()> {
596 if value.is_some_and(|v| v <= 0) {
597 return Err(ScheduleAuthoringError::InvalidValue {
598 entity: "IFCRECURRENCEPATTERN",
599 attribute,
600 expected: "a positive count",
601 });
602 }
603 Ok(())
604}
605
606fn integer_list(values: &[i64]) -> Value {
608 if values.is_empty() {
609 return Value::Null;
610 }
611 Value::List(values.iter().copied().map(Value::Integer).collect())
612}
613
614mod checks;
615mod owned;
616mod procedure;
617mod timing;
618
619pub use owned::{
620 assign_tasks_to_control_with_owner_history, create_event_with_owner_history,
621 create_procedure_with_owner_history, create_sequence_with_owner_history,
622 create_task_with_owner_history, create_work_calendar_with_owner_history,
623 create_work_control_with_owner_history, nest_tasks_with_owner_history,
624};
625pub use procedure::{create_procedure, ProcedureDraft};
626pub use timing::{create_task_time, create_task_time_recurring, create_time_period, TaskTimeDraft};