use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use crate::plan_span::{PlanLength, PlanSpanError, PlanSpanService};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CorridorEndRequest {
space: ObjectId,
subjects: Vec<ObjectId>,
}
impl CorridorEndRequest {
pub fn try_new(
space: ObjectId,
subjects: impl IntoIterator<Item = ObjectId>,
) -> Result<Self, PlanSpanError> {
let mut subjects: Vec<ObjectId> = subjects.into_iter().collect();
subjects.sort();
subjects.dedup();
if subjects.contains(&space) {
return Err(PlanSpanError::Unavailable(format!(
"{space} cannot be measured against its own corridor ends"
)));
}
Ok(Self { space, subjects })
}
#[must_use]
pub fn space(&self) -> &ObjectId {
&self.space
}
#[must_use]
pub fn subjects(&self) -> &[ObjectId] {
&self.subjects
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct WallContact {
subject: ObjectId,
gap: PlanLength,
facing: PlanLength,
}
impl WallContact {
#[must_use]
pub fn new(subject: ObjectId, gap: PlanLength, facing: PlanLength) -> Self {
Self {
subject,
gap,
facing,
}
}
#[must_use]
pub fn subject(&self) -> &ObjectId {
&self.subject
}
#[must_use]
pub fn gap(&self) -> &PlanLength {
&self.gap
}
#[must_use]
pub fn facing(&self) -> &PlanLength {
&self.facing
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum EndWall {
Decided {
start: [f64; 2],
end: [f64; 2],
contacts: Vec<WallContact>,
},
Undecided(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct CorridorEnd {
point: [f64; 2],
clearance: (f64, f64),
wall: EndWall,
}
impl CorridorEnd {
pub fn try_new(
point: [f64; 2],
clearance: (f64, f64),
wall: EndWall,
) -> Result<Self, PlanSpanError> {
let (lower, upper) = clearance;
if !point.iter().all(|value| value.is_finite())
|| !lower.is_finite()
|| !upper.is_finite()
|| lower < 0.0
|| lower > upper
{
return Err(PlanSpanError::InvalidMeasurement);
}
if let EndWall::Decided { start, end, .. } = &wall {
#[allow(clippy::float_cmp)]
let degenerate = start == end;
if degenerate || !start.iter().chain(end).all(|value| value.is_finite()) {
return Err(PlanSpanError::InvalidMeasurement);
}
}
Ok(Self {
point,
clearance,
wall,
})
}
#[must_use]
pub fn point(&self) -> [f64; 2] {
self.point
}
#[must_use]
pub fn clearance_metres(&self) -> (f64, f64) {
self.clearance
}
#[must_use]
pub fn wall(&self) -> &EndWall {
&self.wall
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CorridorEnds {
space: ObjectId,
ends: Vec<CorridorEnd>,
evidence: Evidence,
}
impl CorridorEnds {
pub fn try_new(
space: ObjectId,
ends: Vec<CorridorEnd>,
evidence: Evidence,
) -> Result<Self, PlanSpanError> {
if evidence.exact || evidence.locator.trim().is_empty() {
return Err(PlanSpanError::InexactEvidence);
}
Ok(Self {
space,
ends,
evidence,
})
}
#[must_use]
pub fn space(&self) -> &ObjectId {
&self.space
}
#[must_use]
pub fn ends(&self) -> &[CorridorEnd] {
&self.ends
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub(crate) fn bound(
request: &CorridorEndRequest,
ends: CorridorEnds,
) -> Result<CorridorEnds, PlanSpanError> {
if ends.space() != request.space() {
return Err(PlanSpanError::Unavailable(format!(
"corridor ends of {} were returned for {}",
ends.space(),
request.space()
)));
}
for end in ends.ends() {
if let EndWall::Decided { contacts, .. } = end.wall() {
let named = contacts.iter().map(WallContact::subject);
if !named.eq(request.subjects()) {
return Err(PlanSpanError::Unavailable(format!(
"an end wall of {} was not measured against exactly the requested subjects",
request.space()
)));
}
}
}
Ok(ends)
}
pub(crate) fn measure(
service: &Arc<dyn PlanSpanService>,
request: &CorridorEndRequest,
) -> Result<CorridorEnds, PlanSpanError> {
bound(request, service.measure_corridor_ends(request)?)
}
#[cfg(test)]
mod tests {
use axioval_ir::{Evidence, ObjectId, SourceId};
use super::{CorridorEnd, CorridorEndRequest, CorridorEnds, EndWall, WallContact, bound};
use crate::plan_span::{PlanLength, PlanSpanError};
fn source() -> SourceId {
SourceId::new("cad", "m").unwrap()
}
fn id(local: &str) -> ObjectId {
ObjectId::new(source(), local).unwrap()
}
fn approximate() -> Evidence {
Evidence {
source: source(),
locator: "corridor-ends:hall".into(),
exact: false,
}
}
fn zero() -> PlanLength {
PlanLength::try_new(0.0, 0.0, Evidence::exact(source(), "gap")).unwrap()
}
fn end(contacts: Vec<WallContact>) -> CorridorEnd {
CorridorEnd::try_new(
[0.5, 0.5],
(0.5, 0.5),
EndWall::Decided {
start: [0.0, 1.0],
end: [0.0, 0.0],
contacts,
},
)
.unwrap()
}
#[test]
fn a_request_sorts_its_subjects_and_refuses_the_space() {
let request =
CorridorEndRequest::try_new(id("hall"), [id("w2"), id("w1"), id("w2")]).unwrap();
assert_eq!(request.subjects(), [id("w1"), id("w2")]);
assert!(CorridorEndRequest::try_new(id("hall"), [id("hall")]).is_err());
}
#[test]
fn corridor_ends_are_never_exact() {
let exact = Evidence::exact(source(), "corridor-ends:hall");
assert_eq!(
CorridorEnds::try_new(id("hall"), Vec::new(), exact),
Err(PlanSpanError::InexactEvidence)
);
assert!(CorridorEnds::try_new(id("hall"), Vec::new(), approximate()).is_ok());
}
#[test]
fn an_end_refuses_bad_clearances_and_degenerate_walls() {
let wall = || EndWall::Undecided("unused".into());
assert!(CorridorEnd::try_new([0.0, 0.0], (0.5, 0.4), wall()).is_err());
assert!(CorridorEnd::try_new([0.0, 0.0], (-0.1, 0.4), wall()).is_err());
assert!(CorridorEnd::try_new([f64::NAN, 0.0], (0.1, 0.4), wall()).is_err());
assert!(
CorridorEnd::try_new(
[0.0, 0.0],
(0.1, 0.1),
EndWall::Decided {
start: [1.0, 1.0],
end: [1.0, 1.0],
contacts: Vec::new()
}
)
.is_err()
);
}
#[test]
fn an_answer_is_bound_to_its_space_and_subjects() {
let request = CorridorEndRequest::try_new(id("hall"), [id("w1")]).unwrap();
let answer = |space: &str, contacts| {
CorridorEnds::try_new(id(space), vec![end(contacts)], approximate()).unwrap()
};
let contact = |subject: &str| WallContact::new(id(subject), zero(), zero());
assert!(bound(&request, answer("hall", vec![contact("w1")])).is_ok());
assert!(bound(&request, answer("room", vec![contact("w1")])).is_err());
assert!(bound(&request, answer("hall", vec![contact("w2")])).is_err());
assert!(bound(&request, answer("hall", Vec::new())).is_err());
assert!(bound(&request, answer("hall", vec![contact("w1"), contact("w1")])).is_err());
}
}