#[cfg(test)]
#[path = "../../../tests/unit/construction/features/locked_jobs_test.rs"]
mod locked_jobs_test;
use super::*;
use crate::models::problem::{Actor, Fleet};
use crate::models::{Lock, LockOrder, LockPosition};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
type ConditionMap = HashMap<Job, Arc<dyn Fn(&Actor) -> bool + Sync + Send>>;
pub fn create_locked_jobs_feature(
name: &str,
fleet: &Fleet,
locks: &[Arc<Lock>],
code: ViolationCode,
) -> Result<Feature, GenericError> {
let (rules, conditions) = locks.iter().fold((Vec::new(), HashMap::new()), |(mut rules, mut conditions), lock| {
let condition = lock.condition_fn.clone();
lock.details.iter().for_each(|detail| {
if let LockOrder::Strict = detail.order {
assert!(!detail.jobs.is_empty());
rules.push(Arc::new(Rule {
condition: condition.clone(),
position: detail.position.clone(),
index: JobIndex {
first: detail.jobs.first().unwrap().clone(),
last: detail.jobs.last().unwrap().clone(),
jobs: detail.jobs.iter().cloned().collect(),
},
}));
}
detail.jobs.iter().cloned().collect::<HashSet<Job>>().into_iter().for_each(|job| {
conditions.insert(job, condition.clone());
});
});
(rules, conditions)
});
let rules = fleet.actors.iter().fold(HashMap::new(), |mut acc, actor| {
acc.insert(actor.clone(), rules.iter().filter(|rule| (rule.condition)(actor)).cloned().collect());
acc
});
FeatureBuilder::default().with_name(name).with_constraint(LockingConstraint { code, conditions, rules }).build()
}
struct LockingConstraint {
code: ViolationCode,
conditions: ConditionMap,
rules: HashMap<Arc<Actor>, Vec<Arc<Rule>>>,
}
impl LockingConstraint {
fn evaluate_route(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
if let Some(condition) = self.conditions.get(job) {
if !(condition)(&route_ctx.route().actor) {
return ConstraintViolation::fail(self.code);
}
}
None
}
fn evaluate_activity(
&self,
route_ctx: &RouteContext,
activity_ctx: &ActivityContext,
) -> Option<ConstraintViolation> {
if let Some(rules) = self.rules.get(&route_ctx.route().actor) {
let can_insert = rules.iter().all(|rule| {
rule.can_insert(
&activity_ctx.target.retrieve_job(),
&activity_ctx.prev.retrieve_job(),
&activity_ctx.next.and_then(|n| n.retrieve_job()),
)
});
if !can_insert {
return ConstraintViolation::skip(self.code);
}
}
None
}
}
impl FeatureConstraint for LockingConstraint {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { route_ctx, job, .. } => self.evaluate_route(route_ctx, job),
MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
}
}
fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
if self.conditions.contains_key(&candidate) {
Err(self.code)
} else {
Ok(source)
}
}
}
struct JobIndex {
first: Job,
last: Job,
jobs: HashSet<Job>,
}
struct Rule {
condition: Arc<dyn Fn(&Actor) -> bool + Sync + Send>,
position: LockPosition,
index: JobIndex,
}
impl Rule {
pub fn can_insert(&self, job: &Option<Job>, prev: &Option<Job>, next: &Option<Job>) -> bool {
self.is_in_rule(job)
|| match self.position {
LockPosition::Any => self.can_insert_after(prev, next) || self.can_insert_before(prev, next),
LockPosition::Departure => self.can_insert_after(prev, next),
LockPosition::Arrival => self.can_insert_before(prev, next),
LockPosition::Fixed => false,
}
}
fn contains(&self, job: &Job) -> bool {
self.index.jobs.contains(job)
}
fn is_in_rule(&self, job: &Option<Job>) -> bool {
job.as_ref().map_or(false, |job| self.contains(job))
}
fn can_insert_after(&self, prev: &Option<Job>, next: &Option<Job>) -> bool {
prev.as_ref().map_or(false, |p| !self.contains(p) || *p == self.index.last)
&& next.as_ref().map_or(true, |n| !self.contains(n))
}
fn can_insert_before(&self, prev: &Option<Job>, next: &Option<Job>) -> bool {
next.as_ref().map_or(false, |n| !self.contains(n) || *n == self.index.first)
&& prev.as_ref().map_or(true, |p| !self.contains(p))
}
}