use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use thiserror::Error;
use crate::coverage::{CoverageEvidence, CoverageRequest, check_answer};
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum PlanAreaError {
#[error("no geometry for `{0}`")]
UnknownObject(ObjectId),
#[error("plan area unavailable: {0}")]
Unavailable(String),
#[error("plan area measurement is invalid")]
InvalidMeasurement,
#[error("plan area evidence does not match its exactness")]
InexactEvidence,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlanArea {
lower: f64,
upper: f64,
evidence: Evidence,
}
impl PlanArea {
pub fn try_new(lower: f64, upper: f64, evidence: Evidence) -> Result<Self, PlanAreaError> {
if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
return Err(PlanAreaError::InvalidMeasurement);
}
#[allow(clippy::float_cmp)]
let exact = lower == upper;
if evidence.exact != exact || evidence.locator.trim().is_empty() {
return Err(PlanAreaError::InexactEvidence);
}
Ok(Self {
lower,
upper,
evidence,
})
}
#[must_use]
pub fn lower_square_metres(&self) -> f64 {
self.lower
}
#[must_use]
pub fn upper_square_metres(&self) -> f64 {
self.upper
}
#[must_use]
pub fn is_exact(&self) -> bool {
self.evidence.exact
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlanBand {
first: ObjectId,
second: ObjectId,
direction: [f64; 2],
}
impl PlanBand {
pub fn try_new(
first: ObjectId,
second: ObjectId,
direction: [f64; 2],
) -> Result<Self, PlanAreaError> {
let length = direction[0].hypot(direction[1]);
if first == second {
return Err(PlanAreaError::Unavailable(format!(
"a band needs two objects, not {first} twice"
)));
}
if !length.is_finite() || length <= f64::EPSILON {
return Err(PlanAreaError::Unavailable(format!(
"a band needs a plan direction, not {direction:?}"
)));
}
let (first, second) = if first < second {
(first, second)
} else {
(second, first)
};
Ok(Self {
first,
second,
direction: [direction[0] / length, direction[1] / length],
})
}
#[must_use]
pub fn objects(&self) -> [&ObjectId; 2] {
[&self.first, &self.second]
}
#[must_use]
pub fn direction(&self) -> [f64; 2] {
self.direction
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ElevationRequest {
object: ObjectId,
axis: [f64; 2],
cover: Vec<ObjectId>,
frame: Vec<ObjectId>,
along_growth: f64,
vertical_growth: f64,
}
impl ElevationRequest {
pub fn try_new(
object: ObjectId,
axis: [f64; 2],
cover: &[ObjectId],
frame: &[ObjectId],
along_growth_metres: f64,
vertical_growth_metres: f64,
) -> Result<Self, PlanAreaError> {
let length = axis[0].hypot(axis[1]);
if !length.is_finite() || length <= f64::EPSILON {
return Err(PlanAreaError::Unavailable(format!(
"an elevation needs a plan axis, not {axis:?}"
)));
}
for growth in [along_growth_metres, vertical_growth_metres] {
if !growth.is_finite() || growth < 0.0 {
return Err(PlanAreaError::Unavailable(format!(
"a growth of {growth} m is not a non-negative length"
)));
}
}
if cover.contains(&object) || frame.contains(&object) {
return Err(PlanAreaError::Unavailable(format!(
"{object} cannot cover its own elevation"
)));
}
let sorted = |objects: &[ObjectId]| {
let mut objects = objects.to_vec();
objects.sort();
objects.dedup();
objects
};
Ok(Self {
object,
axis: [axis[0] / length, axis[1] / length],
cover: sorted(cover),
frame: sorted(frame),
along_growth: along_growth_metres,
vertical_growth: vertical_growth_metres,
})
}
#[must_use]
pub fn object(&self) -> &ObjectId {
&self.object
}
#[must_use]
pub fn axis(&self) -> [f64; 2] {
self.axis
}
#[must_use]
pub fn cover(&self) -> &[ObjectId] {
&self.cover
}
#[must_use]
pub fn frame(&self) -> &[ObjectId] {
&self.frame
}
#[must_use]
pub fn along_growth_metres(&self) -> f64 {
self.along_growth
}
#[must_use]
pub fn vertical_growth_metres(&self) -> f64 {
self.vertical_growth
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ElevationCover {
object: ObjectId,
area: (f64, f64),
uncovered: (f64, f64),
evidence: Evidence,
}
impl ElevationCover {
pub fn try_new(
object: ObjectId,
area: (f64, f64),
uncovered: (f64, f64),
evidence: Evidence,
) -> Result<Self, PlanAreaError> {
let valid = |(lower, upper): (f64, f64)| {
lower.is_finite() && upper.is_finite() && lower >= 0.0 && lower <= upper
};
if !valid(area) || !valid(uncovered) || uncovered.0 > area.1 {
return Err(PlanAreaError::InvalidMeasurement);
}
#[allow(clippy::float_cmp)]
let exact = area.0 == area.1 && uncovered.0 == uncovered.1;
if evidence.exact != exact || evidence.locator.trim().is_empty() {
return Err(PlanAreaError::InexactEvidence);
}
Ok(Self {
object,
area,
uncovered,
evidence,
})
}
#[must_use]
pub fn object(&self) -> &ObjectId {
&self.object
}
#[must_use]
pub fn area_square_metres(&self) -> (f64, f64) {
self.area
}
#[must_use]
pub fn uncovered_square_metres(&self) -> (f64, f64) {
self.uncovered
}
#[must_use]
pub fn is_exact(&self) -> bool {
self.evidence.exact
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub trait PlanAreaService: Send + Sync + 'static {
fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError>;
fn measure_plan_overlap(
&self,
first: &ObjectId,
second: &ObjectId,
) -> Result<PlanArea, PlanAreaError>;
fn measure_uncovered_area(
&self,
object: &ObjectId,
cover: &[ObjectId],
growth_metres: f64,
) -> Result<PlanArea, PlanAreaError> {
let _ = (object, cover, growth_metres);
Err(PlanAreaError::Unavailable(
"this plan-area service does not measure uncovered areas".into(),
))
}
fn measure_outside_bands(
&self,
object: &ObjectId,
bands: &[PlanBand],
) -> Result<PlanArea, PlanAreaError> {
let _ = (object, bands);
Err(PlanAreaError::Unavailable(
"this plan-area service does not measure bands".into(),
))
}
fn measure_coverage(
&self,
request: &CoverageRequest,
) -> Result<CoverageEvidence, PlanAreaError> {
Err(PlanAreaError::Unavailable(format!(
"this plan-area service does not measure the coverage of {}",
request.subject()
)))
}
fn measure_elevation_cover(
&self,
request: &ElevationRequest,
) -> Result<ElevationCover, PlanAreaError> {
Err(PlanAreaError::Unavailable(format!(
"this plan-area service does not measure the elevation of {}",
request.object()
)))
}
}
#[derive(Clone)]
pub struct PlanAreaServiceHandle(Arc<dyn PlanAreaService>);
impl PlanAreaServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn PlanAreaService>) -> Self {
Self(service)
}
pub fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError> {
self.0.measure_footprint(object)
}
pub fn measure_plan_overlap(
&self,
first: &ObjectId,
second: &ObjectId,
) -> Result<PlanArea, PlanAreaError> {
self.0.measure_plan_overlap(first, second)
}
pub fn measure_uncovered_area(
&self,
object: &ObjectId,
cover: &[ObjectId],
growth_metres: f64,
) -> Result<PlanArea, PlanAreaError> {
if !growth_metres.is_finite() || growth_metres < 0.0 {
return Err(PlanAreaError::Unavailable(format!(
"a growth of {growth_metres} m is not a non-negative length"
)));
}
if cover.contains(object) {
return Err(PlanAreaError::Unavailable(format!(
"{object} cannot cover its own footprint"
)));
}
let mut cover = cover.to_vec();
cover.sort();
cover.dedup();
self.0.measure_uncovered_area(object, &cover, growth_metres)
}
pub fn measure_outside_bands(
&self,
object: &ObjectId,
bands: &[PlanBand],
) -> Result<PlanArea, PlanAreaError> {
if bands.iter().any(|band| band.objects().contains(&object)) {
return Err(PlanAreaError::Unavailable(format!(
"{object} cannot bound a band over its own footprint"
)));
}
let mut bands = bands.to_vec();
bands.sort_by(|a, b| {
(
a.objects(),
a.direction[0].to_bits(),
a.direction[1].to_bits(),
)
.cmp(&(
b.objects(),
b.direction[0].to_bits(),
b.direction[1].to_bits(),
))
});
bands.dedup();
self.0.measure_outside_bands(object, &bands)
}
pub fn measure_coverage(
&self,
request: &CoverageRequest,
) -> Result<CoverageEvidence, PlanAreaError> {
let answer = self.0.measure_coverage(request)?;
check_answer(request, &answer)?;
Ok(answer)
}
pub fn measure_elevation_cover(
&self,
request: &ElevationRequest,
) -> Result<ElevationCover, PlanAreaError> {
let answer = self.0.measure_elevation_cover(request)?;
if answer.object() != request.object() {
return Err(PlanAreaError::InvalidMeasurement);
}
Ok(answer)
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::{PlanArea, PlanAreaError, PlanAreaService, PlanAreaServiceHandle};
use axioval_ir::{Evidence, ObjectId, SourceId};
#[derive(Default)]
struct FootprintsOnly(Mutex<Vec<Vec<ObjectId>>>);
impl PlanAreaService for FootprintsOnly {
fn measure_footprint(&self, _: &ObjectId) -> Result<PlanArea, PlanAreaError> {
PlanArea::try_new(1.0, 1.0, exact())
}
fn measure_plan_overlap(
&self,
_: &ObjectId,
_: &ObjectId,
) -> Result<PlanArea, PlanAreaError> {
PlanArea::try_new(0.0, 0.0, exact())
}
}
struct Recording(Arc<FootprintsOnly>);
impl PlanAreaService for Recording {
fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError> {
self.0.measure_footprint(object)
}
fn measure_plan_overlap(
&self,
first: &ObjectId,
second: &ObjectId,
) -> Result<PlanArea, PlanAreaError> {
self.0.measure_plan_overlap(first, second)
}
fn measure_uncovered_area(
&self,
_: &ObjectId,
cover: &[ObjectId],
_: f64,
) -> Result<PlanArea, PlanAreaError> {
self.0.0.lock().unwrap().push(cover.to_vec());
PlanArea::try_new(0.5, 0.5, exact())
}
}
fn id(local: &str) -> ObjectId {
ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
}
#[test]
fn a_service_without_uncovered_areas_refuses_rather_than_answering() {
let handle = PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()));
assert!(matches!(
handle.measure_uncovered_area(&id("a"), &[id("b")], 0.0),
Err(PlanAreaError::Unavailable(_))
));
let band = super::PlanBand::try_new(id("b"), id("c"), [1.0, 0.0]).unwrap();
assert!(matches!(
handle.measure_outside_bands(&id("a"), &[band]),
Err(PlanAreaError::Unavailable(_))
));
}
#[test]
#[allow(clippy::float_cmp)]
fn a_band_is_ordered_normalised_and_never_bounded_by_its_subject() {
let band = super::PlanBand::try_new(id("c"), id("b"), [0.0, 2.0]).unwrap();
assert_eq!(band.objects(), [&id("b"), &id("c")]);
assert_eq!(band.direction(), [0.0, 1.0]);
assert!(super::PlanBand::try_new(id("b"), id("b"), [1.0, 0.0]).is_err());
assert!(super::PlanBand::try_new(id("b"), id("c"), [0.0, 0.0]).is_err());
assert!(super::PlanBand::try_new(id("b"), id("c"), [f64::NAN, 1.0]).is_err());
let handle = PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()));
assert!(matches!(
handle.measure_outside_bands(&id("b"), &[band]),
Err(PlanAreaError::Unavailable(message)) if message.contains("its own footprint")
));
}
#[test]
fn the_handle_refuses_a_bad_growth_or_self_cover_and_orders_the_cover() {
let log = Arc::new(FootprintsOnly::default());
let handle = PlanAreaServiceHandle::new(Arc::new(Recording(log.clone())));
for growth in [-0.01, f64::NAN, f64::INFINITY] {
assert!(
handle
.measure_uncovered_area(&id("a"), &[id("b")], growth)
.is_err(),
"{growth}"
);
}
assert!(
handle
.measure_uncovered_area(&id("a"), &[id("b"), id("a")], 0.0)
.is_err()
);
assert!(log.0.lock().unwrap().is_empty(), "refused before measuring");
handle
.measure_uncovered_area(&id("a"), &[id("c"), id("b"), id("c")], 0.1)
.unwrap();
assert_eq!(*log.0.lock().unwrap(), vec![vec![id("b"), id("c")]]);
}
fn exact() -> Evidence {
Evidence::exact(SourceId::new("cad", "m").unwrap(), "footprint:a")
}
#[test]
fn exactness_and_bounds_must_agree() {
assert!(PlanArea::try_new(2.0, 2.0, exact()).is_ok());
assert_eq!(
PlanArea::try_new(1.0, 2.0, exact()),
Err(PlanAreaError::InexactEvidence)
);
let mut approximate = exact();
approximate.exact = false;
assert!(PlanArea::try_new(1.0, 2.0, approximate.clone()).is_ok());
assert_eq!(
PlanArea::try_new(2.0, 2.0, approximate),
Err(PlanAreaError::InexactEvidence)
);
}
#[test]
fn reversed_negative_or_non_finite_bounds_are_refused() {
for (lower, upper) in [
(2.0, 1.0),
(-1.0, 1.0),
(0.0, f64::NAN),
(0.0, f64::INFINITY),
] {
assert_eq!(
PlanArea::try_new(lower, upper, exact()),
Err(PlanAreaError::InvalidMeasurement),
"{lower} {upper}"
);
}
}
#[test]
#[allow(clippy::float_cmp)]
fn an_elevation_request_is_normalised_ordered_and_never_covers_itself() {
use super::ElevationRequest;
let request = ElevationRequest::try_new(
id("a"),
[0.0, 2.0],
&[id("c"), id("b"), id("c")],
&[],
0.1,
0.2,
)
.unwrap();
assert_eq!(request.axis(), [0.0, 1.0]);
assert_eq!(request.cover(), [id("b"), id("c")]);
assert!(request.frame().is_empty());
assert_eq!(
(
request.along_growth_metres(),
request.vertical_growth_metres()
),
(0.1, 0.2)
);
for (axis, along, vertical) in [
([0.0, 0.0], 0.0, 0.0),
([f64::NAN, 1.0], 0.0, 0.0),
([1.0, 0.0], -0.1, 0.0),
([1.0, 0.0], 0.0, f64::INFINITY),
] {
assert!(
ElevationRequest::try_new(id("a"), axis, &[], &[], along, vertical).is_err(),
"{axis:?} {along} {vertical}"
);
}
assert!(ElevationRequest::try_new(id("a"), [1.0, 0.0], &[id("a")], &[], 0.0, 0.0).is_err());
assert!(ElevationRequest::try_new(id("a"), [1.0, 0.0], &[], &[id("a")], 0.0, 0.0).is_err());
}
#[test]
fn an_elevation_cover_must_be_coherent_and_about_the_requested_object() {
use super::ElevationCover;
let mut approximate = exact();
approximate.exact = false;
assert!(ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.0), exact()).is_ok());
assert!(
ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.5), approximate.clone()).is_ok()
);
assert_eq!(
ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.5), exact()),
Err(PlanAreaError::InexactEvidence)
);
assert_eq!(
ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.0), approximate.clone()),
Err(PlanAreaError::InexactEvidence)
);
for (area, uncovered) in [
((2.0, 1.0), (0.0, 0.0)),
((1.0, 1.0), (1.5, 1.5)),
((1.0, 1.0), (-0.5, 0.5)),
((1.0, f64::NAN), (0.0, 0.0)),
] {
assert_eq!(
ElevationCover::try_new(id("a"), area, uncovered, approximate.clone()),
Err(PlanAreaError::InvalidMeasurement),
"{area:?} {uncovered:?}"
);
}
}
#[test]
fn an_elevation_about_another_object_or_unmeasured_is_refused() {
use super::{ElevationCover, ElevationRequest};
struct Elsewhere;
impl PlanAreaService for Elsewhere {
fn measure_footprint(&self, _: &ObjectId) -> Result<PlanArea, PlanAreaError> {
PlanArea::try_new(1.0, 1.0, exact())
}
fn measure_plan_overlap(
&self,
_: &ObjectId,
_: &ObjectId,
) -> Result<PlanArea, PlanAreaError> {
PlanArea::try_new(0.0, 0.0, exact())
}
fn measure_elevation_cover(
&self,
_: &ElevationRequest,
) -> Result<ElevationCover, PlanAreaError> {
ElevationCover::try_new(id("b"), (1.0, 1.0), (0.0, 0.0), exact())
}
}
let request = ElevationRequest::try_new(id("a"), [1.0, 0.0], &[], &[], 0.0, 0.0).unwrap();
assert_eq!(
PlanAreaServiceHandle::new(Arc::new(Elsewhere)).measure_elevation_cover(&request),
Err(PlanAreaError::InvalidMeasurement)
);
assert!(matches!(
PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()))
.measure_elevation_cover(&request),
Err(PlanAreaError::Unavailable(_))
));
}
}