use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use crate::services::reviewable_exact_evidence;
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum SpaceError {
#[error("space quantities must be finite and non-negative")]
InvalidQuantity,
#[error("space evidence must be exact and reviewable")]
InexactEvidence,
#[error("space measurement is unavailable for the requested aspect")]
Unavailable,
}
fn finite_non_negative(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
#[derive(Clone, Debug, PartialEq)]
pub struct ClearHeightEvidence {
space: ObjectId,
metres: f64,
evidence: Evidence,
}
impl ClearHeightEvidence {
pub fn try_new(space: ObjectId, metres: f64, evidence: Evidence) -> Result<Self, SpaceError> {
if !finite_non_negative(metres) {
return Err(SpaceError::InvalidQuantity);
}
if !reviewable_exact_evidence(&evidence) {
return Err(SpaceError::InexactEvidence);
}
Ok(Self {
space,
metres,
evidence,
})
}
pub fn space(&self) -> &ObjectId {
&self.space
}
pub fn metres(&self) -> f64 {
self.metres
}
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundaryGap {
length_metres: f64,
elements: Vec<ObjectId>,
}
impl BoundaryGap {
pub fn try_new(length_metres: f64, mut elements: Vec<ObjectId>) -> Result<Self, SpaceError> {
if !finite_non_negative(length_metres) {
return Err(SpaceError::InvalidQuantity);
}
elements.sort();
elements.dedup();
Ok(Self {
length_metres,
elements,
})
}
pub fn length_metres(&self) -> f64 {
self.length_metres
}
pub fn elements(&self) -> &[ObjectId] {
&self.elements
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Containment {
Partial,
SubjectInsideOther,
OtherInsideSubject,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SpaceOverlap {
other: ObjectId,
other_is_space: bool,
area_square_metres: f64,
height_metres: f64,
containment: Containment,
}
impl SpaceOverlap {
pub fn try_new(
other: ObjectId,
other_is_space: bool,
area_square_metres: f64,
height_metres: f64,
containment: Containment,
) -> Result<Self, SpaceError> {
if !finite_non_negative(area_square_metres) || !finite_non_negative(height_metres) {
return Err(SpaceError::InvalidQuantity);
}
Ok(Self {
other,
other_is_space,
area_square_metres,
height_metres,
containment,
})
}
pub fn other(&self) -> &ObjectId {
&self.other
}
pub fn other_is_space(&self) -> bool {
self.other_is_space
}
pub fn area_square_metres(&self) -> f64 {
self.area_square_metres
}
pub fn height_metres(&self) -> f64 {
self.height_metres
}
pub fn containment(&self) -> Containment {
self.containment
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CapCoverage {
whole_area_square_metres: f64,
covered_area_square_metres: f64,
elements: Vec<ObjectId>,
}
impl CapCoverage {
pub fn try_new(
whole_area_square_metres: f64,
covered_area_square_metres: f64,
mut elements: Vec<ObjectId>,
) -> Result<Self, SpaceError> {
if !finite_non_negative(whole_area_square_metres)
|| !finite_non_negative(covered_area_square_metres)
|| whole_area_square_metres <= 0.0
|| covered_area_square_metres > whole_area_square_metres
{
return Err(SpaceError::InvalidQuantity);
}
elements.sort();
elements.dedup();
Ok(Self {
whole_area_square_metres,
covered_area_square_metres,
elements,
})
}
pub fn whole_area_square_metres(&self) -> f64 {
self.whole_area_square_metres
}
pub fn covered_area_square_metres(&self) -> f64 {
self.covered_area_square_metres
}
pub fn elements(&self) -> &[ObjectId] {
&self.elements
}
pub fn covered_ratio(&self) -> f64 {
self.covered_area_square_metres / self.whole_area_square_metres
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StoreyResidual {
storey: ObjectId,
area_square_metres: f64,
elements: Vec<ObjectId>,
}
impl StoreyResidual {
pub fn try_new(
storey: ObjectId,
area_square_metres: f64,
mut elements: Vec<ObjectId>,
) -> Result<Self, SpaceError> {
if !finite_non_negative(area_square_metres) {
return Err(SpaceError::InvalidQuantity);
}
elements.sort();
elements.dedup();
Ok(Self {
storey,
area_square_metres,
elements,
})
}
pub fn storey(&self) -> &ObjectId {
&self.storey
}
pub fn area_square_metres(&self) -> f64 {
self.area_square_metres
}
pub fn elements(&self) -> &[ObjectId] {
&self.elements
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SupportCounts {
slabs: usize,
roofs: usize,
buildings: Vec<ObjectId>,
}
impl SupportCounts {
pub fn new(slabs: usize, roofs: usize, mut buildings: Vec<ObjectId>) -> Self {
buildings.sort();
buildings.dedup();
Self {
slabs,
roofs,
buildings,
}
}
pub fn slabs(&self) -> usize {
self.slabs
}
pub fn roofs(&self) -> usize {
self.roofs
}
pub fn buildings(&self) -> &[ObjectId] {
&self.buildings
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cap {
Top,
Bottom,
}
pub trait SpaceService: Send + Sync + 'static {
fn measure_duplicates(&self, space: &ObjectId) -> Result<Vec<ObjectId>, SpaceError>;
fn measure_clear_height(&self, space: &ObjectId) -> Result<ClearHeightEvidence, SpaceError>;
fn measure_boundary_gaps(&self, space: &ObjectId) -> Result<Vec<BoundaryGap>, SpaceError>;
fn measure_overlaps(&self, space: &ObjectId) -> Result<Vec<SpaceOverlap>, SpaceError>;
fn measure_cap_coverage(&self, space: &ObjectId, cap: Cap) -> Result<CapCoverage, SpaceError>;
fn measure_storey_residuals(&self) -> Result<Vec<StoreyResidual>, SpaceError>;
fn measure_support_counts(&self) -> Result<SupportCounts, SpaceError>;
fn evidence(&self) -> Evidence;
}
#[derive(Clone)]
pub struct SpaceServiceHandle(Arc<dyn SpaceService>);
impl SpaceServiceHandle {
pub fn new(service: Arc<dyn SpaceService>) -> Self {
Self(service)
}
pub fn get(&self) -> &dyn SpaceService {
self.0.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use axioval_ir::SourceId;
fn source() -> SourceId {
SourceId::new("cad", "m").unwrap()
}
fn oid(local: &str) -> ObjectId {
ObjectId::new(source(), local).unwrap()
}
#[test]
fn cap_covered_over_its_own_area_is_refused() {
assert_eq!(
CapCoverage::try_new(10.0, 11.0, Vec::new()),
Err(SpaceError::InvalidQuantity)
);
}
#[test]
fn zero_cap_area_is_refused_so_the_ratio_cannot_divide_by_zero() {
assert_eq!(
CapCoverage::try_new(0.0, 0.0, Vec::new()),
Err(SpaceError::InvalidQuantity)
);
}
#[test]
fn cap_ratio_is_exact() {
let coverage = CapCoverage::try_new(4.0, 1.0, Vec::new()).unwrap();
assert!((coverage.covered_ratio() - 0.25).abs() < f64::EPSILON);
}
#[test]
fn element_lists_are_normalised() {
let gap = BoundaryGap::try_new(1.0, vec![oid("w2"), oid("w1"), oid("w2")]).unwrap();
assert_eq!(gap.elements(), &[oid("w1"), oid("w2")]);
}
#[test]
fn non_finite_quantities_are_refused() {
assert!(
ClearHeightEvidence::try_new(oid("s"), f64::NAN, Evidence::exact(source(), "h"))
.is_err()
);
assert!(BoundaryGap::try_new(f64::INFINITY, Vec::new()).is_err());
assert!(SpaceOverlap::try_new(oid("o"), false, -1.0, 1.0, Containment::Partial).is_err());
assert!(StoreyResidual::try_new(oid("st"), f64::NAN, Vec::new()).is_err());
}
#[test]
fn inexact_evidence_is_refused() {
assert_eq!(
ClearHeightEvidence::try_new(
oid("s"),
2.5,
Evidence {
source: source(),
locator: "h".into(),
exact: false,
},
),
Err(SpaceError::InexactEvidence)
);
}
}