use axioval_ir::{Evidence, ObjectId};
use crate::{PlanArea, PlanAreaError};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EffectReach {
Grown,
Travel,
Visible,
}
impl EffectReach {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Grown => "grown",
Self::Travel => "travel",
Self::Visible => "visible",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Participant {
object: ObjectId,
certain: bool,
}
impl Participant {
#[must_use]
pub fn new(object: ObjectId, certain: bool) -> Self {
Self { object, certain }
}
#[must_use]
pub fn object(&self) -> &ObjectId {
&self.object
}
#[must_use]
pub fn is_certain(&self) -> bool {
self.certain
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CoverageRequest {
subject: ObjectId,
reach: EffectReach,
range: f64,
sources: Vec<Participant>,
blockers: Vec<Participant>,
connected: Vec<Participant>,
passages: Vec<Participant>,
}
impl CoverageRequest {
pub fn try_new(
subject: ObjectId,
reach: EffectReach,
range_metres: f64,
sources: Vec<Participant>,
blockers: Vec<Participant>,
) -> Result<Self, PlanAreaError> {
if !range_metres.is_finite() || range_metres < 0.0 {
return Err(PlanAreaError::Unavailable(format!(
"a range of {range_metres} m is not a non-negative length"
)));
}
let sources = merged(sources);
let blockers = merged(blockers);
let named = |list: &[Participant], object: &ObjectId| {
list.iter()
.any(|participant| participant.object() == object)
};
if named(&sources, &subject) || named(&blockers, &subject) {
return Err(PlanAreaError::Unavailable(format!(
"{subject} cannot cover or block its own footprint"
)));
}
if let Some(both) = sources
.iter()
.find(|source| named(&blockers, source.object()))
{
return Err(PlanAreaError::Unavailable(format!(
"{} is both a source and a blocker",
both.object()
)));
}
Ok(Self {
subject,
reach,
range: range_metres,
sources,
blockers,
connected: Vec::new(),
passages: Vec::new(),
})
}
pub fn with_connections(
mut self,
connected: Vec<Participant>,
passages: Vec<Participant>,
) -> Result<Self, PlanAreaError> {
if self.reach == EffectReach::Grown && !(connected.is_empty() && passages.is_empty()) {
return Err(PlanAreaError::Unavailable(
"a grown effect ignores walls, so it continues through no connection".into(),
));
}
let connected = merged(connected);
let passages = merged(passages);
let taken = |object: &ObjectId| {
*object == self.subject
|| self
.sources
.iter()
.chain(&self.blockers)
.any(|participant| participant.object() == object)
};
if let Some(clash) = connected
.iter()
.chain(&passages)
.find(|participant| taken(participant.object()))
{
return Err(PlanAreaError::Unavailable(format!(
"{} cannot be a connection and the subject, a source or a blocker",
clash.object()
)));
}
if let Some(both) = connected.iter().find(|space| {
passages
.iter()
.any(|passage| passage.object() == space.object())
}) {
return Err(PlanAreaError::Unavailable(format!(
"{} is both a connected space and a passage",
both.object()
)));
}
self.connected = connected;
self.passages = passages;
Ok(self)
}
#[must_use]
pub fn subject(&self) -> &ObjectId {
&self.subject
}
#[must_use]
pub fn reach(&self) -> EffectReach {
self.reach
}
#[must_use]
pub fn range_metres(&self) -> f64 {
self.range
}
#[must_use]
pub fn sources(&self) -> &[Participant] {
&self.sources
}
#[must_use]
pub fn blockers(&self) -> &[Participant] {
&self.blockers
}
#[must_use]
pub fn connected(&self) -> &[Participant] {
&self.connected
}
#[must_use]
pub fn passages(&self) -> &[Participant] {
&self.passages
}
}
fn merged(mut list: Vec<Participant>) -> Vec<Participant> {
list.sort_by(|a, b| a.object.cmp(&b.object).then(b.certain.cmp(&a.certain)));
list.dedup_by(|later, kept| later.object == kept.object);
list
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EffectMeets {
Surely,
Possibly,
No,
Unmeasured(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct CoverageEvidence {
subject: ObjectId,
footprint: PlanArea,
covered: PlanArea,
effects: Vec<(ObjectId, EffectMeets)>,
}
impl CoverageEvidence {
pub fn try_new(
subject: ObjectId,
footprint: PlanArea,
covered: PlanArea,
effects: Vec<(ObjectId, EffectMeets)>,
) -> Result<Self, PlanAreaError> {
if covered.lower_square_metres() > footprint.upper_square_metres() {
return Err(PlanAreaError::InvalidMeasurement);
}
Ok(Self {
subject,
footprint,
covered,
effects,
})
}
#[must_use]
pub fn subject(&self) -> &ObjectId {
&self.subject
}
#[must_use]
pub fn footprint(&self) -> &PlanArea {
&self.footprint
}
#[must_use]
pub fn covered(&self) -> &PlanArea {
&self.covered
}
#[must_use]
pub fn effects(&self) -> &[(ObjectId, EffectMeets)] {
&self.effects
}
#[must_use]
pub fn evidence(&self) -> [&Evidence; 2] {
[self.footprint.evidence(), self.covered.evidence()]
}
}
pub(crate) fn check_answer(
request: &CoverageRequest,
answer: &CoverageEvidence,
) -> Result<(), PlanAreaError> {
if answer.subject() != request.subject() {
return Err(PlanAreaError::Unavailable(format!(
"coverage of {} was returned for {}",
answer.subject(),
request.subject()
)));
}
let answered = answer.effects().iter().map(|(object, _)| object);
let asked = request.sources().iter().map(Participant::object);
if !answered.eq(asked) {
return Err(PlanAreaError::Unavailable(
"the coverage answer does not list the requested sources".into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use axioval_ir::SourceId;
fn id(local: &str) -> ObjectId {
ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
}
fn area(value: f64) -> PlanArea {
PlanArea::try_new(
value,
value,
Evidence::exact(SourceId::new("cad", "m").unwrap(), "area"),
)
.unwrap()
}
#[test]
fn a_request_merges_listings_and_refuses_overlaps_and_bad_ranges() {
let source = |local, certain| Participant::new(id(local), certain);
let request = CoverageRequest::try_new(
id("s"),
EffectReach::Grown,
1.0,
vec![source("b", false), source("a", false), source("b", true)],
vec![],
)
.unwrap();
assert_eq!(request.sources(), [source("a", false), source("b", true)]);
for range in [-1.0, f64::NAN] {
assert!(
CoverageRequest::try_new(id("s"), EffectReach::Grown, range, vec![], vec![])
.is_err()
);
}
assert!(
CoverageRequest::try_new(
id("s"),
EffectReach::Travel,
1.0,
vec![source("s", true)],
vec![]
)
.is_err()
);
assert!(
CoverageRequest::try_new(
id("s"),
EffectReach::Travel,
1.0,
vec![source("a", true)],
vec![source("a", false)]
)
.is_err()
);
}
#[test]
fn connections_are_merged_and_refused_where_they_overlap() {
let one = |local, certain| Participant::new(id(local), certain);
let request = || {
CoverageRequest::try_new(
id("s"),
EffectReach::Travel,
5.0,
vec![one("a", true)],
vec![one("w", true)],
)
.unwrap()
};
let joined = request()
.with_connections(vec![one("n", false), one("n", true)], vec![one("d", false)])
.unwrap();
assert_eq!(joined.connected(), [one("n", true)]);
assert_eq!(joined.passages(), [one("d", false)]);
assert!(request().connected().is_empty() && request().passages().is_empty());
for (connected, passages) in [
(vec![one("s", true)], vec![]),
(vec![one("a", true)], vec![]),
(vec![], vec![one("w", false)]),
(vec![one("n", true)], vec![one("n", true)]),
] {
assert!(request().with_connections(connected, passages).is_err());
}
let grown =
CoverageRequest::try_new(id("s"), EffectReach::Grown, 1.0, vec![], vec![]).unwrap();
assert!(
grown
.clone()
.with_connections(vec![one("n", true)], vec![])
.is_err()
);
assert!(grown.with_connections(vec![], vec![]).is_ok());
}
#[test]
fn an_answer_must_fit_its_request() {
let request = CoverageRequest::try_new(
id("s"),
EffectReach::Grown,
1.0,
vec![Participant::new(id("a"), true)],
vec![],
)
.unwrap();
assert!(CoverageEvidence::try_new(id("s"), area(1.0), area(2.0), vec![]).is_err());
let wrong = CoverageEvidence::try_new(id("s"), area(1.0), area(0.5), vec![]).unwrap();
assert!(check_answer(&request, &wrong).is_err());
let right = CoverageEvidence::try_new(
id("s"),
area(1.0),
area(0.5),
vec![(id("a"), EffectMeets::Surely)],
)
.unwrap();
assert!(check_answer(&request, &right).is_ok());
}
}