use std::collections::HashSet;
use ifc_model::{EntityId, Model, Value};
pub(crate) mod slot {
pub const RELATING: usize = 4;
pub const RELATED: usize = 5;
pub const TIME_LAG: usize = 6;
pub const SEQUENCE_TYPE: usize = 7;
}
pub mod lag_slot {
pub const LAG_VALUE: usize = 3;
pub const DURATION_TYPE: usize = 4;
}
pub const MAX_SEQUENCE_DEPTH: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SequenceType {
StartStart,
StartFinish,
FinishStart,
FinishFinish,
UserDefined,
NotDefined,
}
impl SequenceType {
fn parse(token: &str) -> Option<Self> {
Some(match token {
"START_START" => Self::StartStart,
"START_FINISH" => Self::StartFinish,
"FINISH_START" => Self::FinishStart,
"FINISH_FINISH" => Self::FinishFinish,
"USERDEFINED" => Self::UserDefined,
"NOTDEFINED" => Self::NotDefined,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Lag {
pub id: EntityId,
pub duration: Option<String>,
pub ratio: Option<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Sequence {
pub id: EntityId,
pub predecessor: EntityId,
pub successor: EntityId,
pub sequence_type: Option<SequenceType>,
pub lag: Option<Lag>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SequenceCycle {
pub repeated: EntityId,
pub path: Vec<EntityId>,
}
#[must_use]
pub fn sequences(model: &Model) -> Vec<Sequence> {
let mut out = Vec::new();
for (id, entity) in model.of_type("IFCRELSEQUENCE") {
let (Some(Value::Ref(predecessor)), Some(Value::Ref(successor))) = (
entity.attribute(slot::RELATING),
entity.attribute(slot::RELATED),
) else {
continue;
};
let sequence_type = match entity.attribute(slot::SEQUENCE_TYPE) {
Some(Value::Enum(token)) => SequenceType::parse(token),
_ => None,
};
let lag = match entity.attribute(slot::TIME_LAG) {
Some(Value::Ref(lag_id)) => read_lag(model, *lag_id),
_ => None,
};
out.push(Sequence {
id,
predecessor: *predecessor,
successor: *successor,
sequence_type,
lag,
});
}
out
}
fn read_lag(model: &Model, id: EntityId) -> Option<Lag> {
let entity = model.get(id)?;
if !entity.type_name.eq_ignore_ascii_case("IFCLAGTIME") {
return None;
}
let value = entity.attribute(lag_slot::LAG_VALUE);
let duration = value
.and_then(|v| v.unwrap_typed().as_text())
.map(str::to_string);
let ratio = if duration.is_some() {
None
} else {
value.and_then(|v| v.unwrap_typed().as_f64())
};
let _ = entity.attribute(lag_slot::DURATION_TYPE);
Some(Lag {
id,
duration,
ratio,
})
}
#[must_use]
pub fn predecessors_of(model: &Model, task: EntityId) -> Vec<EntityId> {
sequences(model)
.into_iter()
.filter(|s| s.successor == task)
.map(|s| s.predecessor)
.collect()
}
#[must_use]
pub fn successors_of(model: &Model, task: EntityId) -> Vec<EntityId> {
sequences(model)
.into_iter()
.filter(|s| s.predecessor == task)
.map(|s| s.successor)
.collect()
}
pub fn downstream_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, SequenceCycle> {
let all = sequences(model);
let mut out = Vec::new();
let mut path = Vec::new();
let mut on_path = HashSet::new();
let mut seen = HashSet::new();
walk(&all, task, &mut out, &mut path, &mut on_path, &mut seen)?;
Ok(out)
}
fn walk(
all: &[Sequence],
node: EntityId,
out: &mut Vec<EntityId>,
path: &mut Vec<EntityId>,
on_path: &mut HashSet<EntityId>,
seen: &mut HashSet<EntityId>,
) -> Result<(), SequenceCycle> {
if path.len() >= MAX_SEQUENCE_DEPTH {
return Ok(());
}
path.push(node);
on_path.insert(node);
for successor in all
.iter()
.filter(|s| s.predecessor == node)
.map(|s| s.successor)
{
if on_path.contains(&successor) {
let mut cycle = path.clone();
cycle.push(successor);
return Err(SequenceCycle {
repeated: successor,
path: cycle,
});
}
if seen.insert(successor) {
out.push(successor);
walk(all, successor, out, path, on_path, seen)?;
}
}
path.pop();
on_path.remove(&node);
Ok(())
}
#[must_use]
pub fn find_cycle(model: &Model) -> Option<SequenceCycle> {
for (id, _) in model.of_type("IFCTASK") {
if let Err(cycle) = downstream_of(model, id) {
return Some(cycle);
}
}
None
}