use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use crate::plan_span::{
PlanLength, PlanRectangle, PlanSpanError, PlanSpanService, RectangleOrientation,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RectangleSide {
AlongFirst,
AlongSecond,
AgainstFirst,
AgainstSecond,
}
impl RectangleSide {
pub const ALL: [Self; 4] = [
Self::AlongFirst,
Self::AlongSecond,
Self::AgainstFirst,
Self::AgainstSecond,
];
#[must_use]
pub fn axis(self) -> usize {
match self {
Self::AlongFirst | Self::AgainstFirst => 0,
Self::AlongSecond | Self::AgainstSecond => 1,
}
}
#[must_use]
pub fn sign(self) -> f64 {
match self {
Self::AlongFirst | Self::AlongSecond => 1.0,
Self::AgainstFirst | Self::AgainstSecond => -1.0,
}
}
#[must_use]
pub fn opposite(self) -> Self {
match self {
Self::AlongFirst => Self::AgainstFirst,
Self::AlongSecond => Self::AgainstSecond,
Self::AgainstFirst => Self::AlongFirst,
Self::AgainstSecond => Self::AlongSecond,
}
}
#[must_use]
pub fn outward(self, rectangle: &PlanRectangle) -> [f64; 2] {
rectangle.axes()[self.axis()].map(|value| self.sign() * value)
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::AlongFirst => "+first",
Self::AlongSecond => "+second",
Self::AgainstFirst => "-first",
Self::AgainstSecond => "-second",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SideDistanceRequest {
object: ObjectId,
candidates: Vec<ObjectId>,
reach: f64,
inset: f64,
}
impl SideDistanceRequest {
pub fn try_new(
object: ObjectId,
candidates: impl IntoIterator<Item = ObjectId>,
reach: f64,
inset: f64,
) -> Result<Self, PlanSpanError> {
let mut candidates: Vec<ObjectId> = candidates.into_iter().collect();
candidates.sort();
candidates.dedup();
if candidates.contains(&object) {
return Err(PlanSpanError::Unavailable(format!(
"{object} cannot lie beside itself"
)));
}
if !(reach.is_finite() && reach > 0.0 && inset.is_finite() && inset >= 0.0) {
return Err(PlanSpanError::Unavailable(
"a side-distance reach must be positive and its inset non-negative".into(),
));
}
Ok(Self {
object,
candidates,
reach,
inset,
})
}
#[must_use]
pub fn object(&self) -> &ObjectId {
&self.object
}
#[must_use]
pub fn candidates(&self) -> &[ObjectId] {
&self.candidates
}
#[must_use]
pub fn reach_metres(&self) -> f64 {
self.reach
}
#[must_use]
pub fn inset_metres(&self) -> f64 {
self.inset
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SidePresence {
Sure,
Possible,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SideDistance {
candidate: ObjectId,
side: RectangleSide,
presence: SidePresence,
distance: PlanLength,
}
impl SideDistance {
#[must_use]
pub fn new(
candidate: ObjectId,
side: RectangleSide,
presence: SidePresence,
distance: PlanLength,
) -> Self {
Self {
candidate,
side,
presence,
distance,
}
}
#[must_use]
pub fn candidate(&self) -> &ObjectId {
&self.candidate
}
#[must_use]
pub fn side(&self) -> RectangleSide {
self.side
}
#[must_use]
pub fn presence(&self) -> SidePresence {
self.presence
}
#[must_use]
pub fn distance(&self) -> &PlanLength {
&self.distance
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SideDistances {
request: SideDistanceRequest,
rectangle: PlanRectangle,
distances: Vec<SideDistance>,
evidence: Evidence,
}
impl SideDistances {
pub fn try_new(
request: SideDistanceRequest,
rectangle: PlanRectangle,
mut distances: Vec<SideDistance>,
evidence: Evidence,
) -> Result<Self, PlanSpanError> {
if rectangle.object() != request.object() {
return Err(PlanSpanError::Unavailable(format!(
"side distances from a rectangle of {} were returned for {}",
rectangle.object(),
request.object()
)));
}
if rectangle.orientation() != RectangleOrientation::Unique {
return Err(PlanSpanError::Unavailable(format!(
"the least-area rectangle of {} is {}, so its sides are not the footprint's own",
request.object(),
rectangle.orientation().name()
)));
}
distances.sort_by(|a, b| (a.side, &a.candidate).cmp(&(b.side, &b.candidate)));
let repeated = distances
.windows(2)
.any(|pair| pair[0].side == pair[1].side && pair[0].candidate == pair[1].candidate);
let unrequested = distances.iter().any(|distance| {
request
.candidates
.binary_search(&distance.candidate)
.is_err()
});
if repeated || unrequested {
return Err(PlanSpanError::Unavailable(format!(
"side distances of {} name a candidate twice or one not requested",
request.object()
)));
}
let exact = rectangle.is_exact()
&& distances.iter().all(|distance| {
distance.distance.is_exact() && distance.presence == SidePresence::Sure
});
if (evidence.exact && !exact) || evidence.locator.trim().is_empty() {
return Err(PlanSpanError::InexactEvidence);
}
Ok(Self {
request,
rectangle,
distances,
evidence,
})
}
#[must_use]
pub fn request(&self) -> &SideDistanceRequest {
&self.request
}
#[must_use]
pub fn rectangle(&self) -> &PlanRectangle {
&self.rectangle
}
#[must_use]
pub fn distances(&self) -> &[SideDistance] {
&self.distances
}
pub fn beside(&self, side: RectangleSide) -> impl Iterator<Item = &SideDistance> {
self.distances
.iter()
.filter(move |distance| distance.side == side)
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub(crate) fn measure(
service: &Arc<dyn PlanSpanService>,
request: &SideDistanceRequest,
) -> Result<SideDistances, PlanSpanError> {
let answer = service.measure_side_distances(request)?;
if answer.request() != request {
return Err(PlanSpanError::Unavailable(format!(
"side distances for another request were returned for {}",
request.object()
)));
}
Ok(answer)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId, SourceId};
use super::{RectangleSide, SideDistance, SideDistanceRequest, SideDistances, SidePresence};
use crate::plan_span::{
PlanLength, PlanRectangle, PlanSpanError, PlanSpanService, PlanSpanServiceHandle,
RectangleOrientation,
};
fn id(local: &str) -> ObjectId {
ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
}
fn evidence(exact: bool) -> Evidence {
Evidence {
source: SourceId::new("cad", "m").unwrap(),
locator: "test".into(),
exact,
}
}
fn rectangle(orientation: RectangleOrientation) -> PlanRectangle {
PlanRectangle::try_new(
id("wc"),
[1.2, 0.35],
0.0,
[[1.0, 0.0], [0.0, 1.0]],
0.0,
[(0.2, 0.2), (0.35, 0.35)],
orientation,
evidence(orientation == RectangleOrientation::Unique),
)
.unwrap()
}
fn beside(candidate: &str) -> SideDistance {
SideDistance::new(
id(candidate),
RectangleSide::AgainstSecond,
SidePresence::Sure,
PlanLength::try_new(0.34, 0.36, evidence(false)).unwrap(),
)
}
fn request() -> SideDistanceRequest {
SideDistanceRequest::try_new(id("wc"), [id("wall")], 1.0, 0.0).unwrap()
}
#[test]
fn requests_refuse_the_object_itself_and_bad_lengths() {
assert!(SideDistanceRequest::try_new(id("wc"), [id("wc")], 1.0, 0.0).is_err());
assert!(SideDistanceRequest::try_new(id("wc"), [], 0.0, 0.0).is_err());
assert!(SideDistanceRequest::try_new(id("wc"), [], 1.0, -0.1).is_err());
}
#[test]
fn answers_need_a_unique_rectangle_and_requested_candidates_once() {
let unique = rectangle(RectangleOrientation::Unique);
assert!(
SideDistances::try_new(
request(),
unique.clone(),
vec![beside("wall")],
evidence(false)
)
.is_ok()
);
assert!(
SideDistances::try_new(
request(),
rectangle(RectangleOrientation::Tied),
vec![beside("wall")],
evidence(false)
)
.is_err()
);
assert!(
SideDistances::try_new(
request(),
unique.clone(),
vec![beside("door")],
evidence(false)
)
.is_err()
);
assert!(
SideDistances::try_new(
request(),
unique.clone(),
vec![beside("wall"), beside("wall")],
evidence(false)
)
.is_err()
);
assert!(
SideDistances::try_new(request(), unique, vec![beside("wall")], evidence(true))
.is_err()
);
let out = RectangleSide::AgainstSecond.outward(&rectangle(RectangleOrientation::Unique));
assert!(
out[0].abs() < 1e-12 && (out[1] + 1.0).abs() < 1e-12,
"{out:?}"
);
}
struct Farther;
impl PlanSpanService for Farther {
fn measure_diameter(&self, _: &ObjectId) -> Result<PlanLength, PlanSpanError> {
Err(PlanSpanError::Unavailable("unused".into()))
}
fn measure_span(
&self,
_: &ObjectId,
_: &ObjectId,
_: crate::plan_span::PlanSpan,
) -> Result<PlanLength, PlanSpanError> {
Err(PlanSpanError::Unavailable("unused".into()))
}
fn measure_side_distances(
&self,
request: &SideDistanceRequest,
) -> Result<SideDistances, PlanSpanError> {
let farther = SideDistanceRequest::try_new(
request.object().clone(),
request.candidates().to_vec(),
request.reach_metres() + 1.0,
request.inset_metres(),
)?;
SideDistances::try_new(
farther,
rectangle(RectangleOrientation::Unique),
Vec::new(),
evidence(false),
)
}
}
#[test]
fn the_handle_refuses_an_answer_to_another_request() {
let handle = PlanSpanServiceHandle::new(Arc::new(Farther));
assert!(matches!(
handle.measure_side_distances(&request()),
Err(PlanSpanError::Unavailable(message)) if message.contains("another request")
));
}
}