1use std::sync::Arc;
15
16use axioval_ir::{Evidence, ObjectId};
17
18use crate::services::reviewable_exact_evidence;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
22pub enum SpaceError {
23 #[error("space quantities must be finite and non-negative")]
25 InvalidQuantity,
26 #[error("space evidence must be exact and reviewable")]
28 InexactEvidence,
29 #[error("space measurement is unavailable for the requested aspect")]
31 Unavailable,
32}
33
34fn finite_non_negative(value: f64) -> bool {
35 value.is_finite() && value >= 0.0
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub struct ClearHeightEvidence {
41 space: ObjectId,
42 metres: f64,
43 evidence: Evidence,
44}
45
46impl ClearHeightEvidence {
47 pub fn try_new(space: ObjectId, metres: f64, evidence: Evidence) -> Result<Self, SpaceError> {
48 if !finite_non_negative(metres) {
49 return Err(SpaceError::InvalidQuantity);
50 }
51 if !reviewable_exact_evidence(&evidence) {
52 return Err(SpaceError::InexactEvidence);
53 }
54 Ok(Self {
55 space,
56 metres,
57 evidence,
58 })
59 }
60 pub fn space(&self) -> &ObjectId {
61 &self.space
62 }
63 pub fn metres(&self) -> f64 {
64 self.metres
65 }
66 pub fn evidence(&self) -> &Evidence {
67 &self.evidence
68 }
69}
70
71#[derive(Clone, Debug, PartialEq)]
73pub struct BoundaryGap {
74 length_metres: f64,
75 elements: Vec<ObjectId>,
76}
77
78impl BoundaryGap {
79 pub fn try_new(length_metres: f64, mut elements: Vec<ObjectId>) -> Result<Self, SpaceError> {
80 if !finite_non_negative(length_metres) {
81 return Err(SpaceError::InvalidQuantity);
82 }
83 elements.sort();
84 elements.dedup();
85 Ok(Self {
86 length_metres,
87 elements,
88 })
89 }
90 pub fn length_metres(&self) -> f64 {
91 self.length_metres
92 }
93 pub fn elements(&self) -> &[ObjectId] {
94 &self.elements
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum Containment {
101 Partial,
103 SubjectInsideOther,
105 OtherInsideSubject,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub struct SpaceOverlap {
112 other: ObjectId,
113 other_is_space: bool,
114 area_square_metres: f64,
115 height_metres: f64,
116 containment: Containment,
117}
118
119impl SpaceOverlap {
120 pub fn try_new(
121 other: ObjectId,
122 other_is_space: bool,
123 area_square_metres: f64,
124 height_metres: f64,
125 containment: Containment,
126 ) -> Result<Self, SpaceError> {
127 if !finite_non_negative(area_square_metres) || !finite_non_negative(height_metres) {
128 return Err(SpaceError::InvalidQuantity);
129 }
130 Ok(Self {
131 other,
132 other_is_space,
133 area_square_metres,
134 height_metres,
135 containment,
136 })
137 }
138 pub fn other(&self) -> &ObjectId {
139 &self.other
140 }
141 pub fn other_is_space(&self) -> bool {
143 self.other_is_space
144 }
145 pub fn area_square_metres(&self) -> f64 {
146 self.area_square_metres
147 }
148 pub fn height_metres(&self) -> f64 {
149 self.height_metres
150 }
151 pub fn containment(&self) -> Containment {
152 self.containment
153 }
154}
155
156#[derive(Clone, Debug, PartialEq)]
158pub struct CapCoverage {
159 whole_area_square_metres: f64,
160 covered_area_square_metres: f64,
161 elements: Vec<ObjectId>,
162}
163
164impl CapCoverage {
165 pub fn try_new(
166 whole_area_square_metres: f64,
167 covered_area_square_metres: f64,
168 mut elements: Vec<ObjectId>,
169 ) -> Result<Self, SpaceError> {
170 if !finite_non_negative(whole_area_square_metres)
171 || !finite_non_negative(covered_area_square_metres)
172 || whole_area_square_metres <= 0.0
173 || covered_area_square_metres > whole_area_square_metres
175 {
176 return Err(SpaceError::InvalidQuantity);
177 }
178 elements.sort();
179 elements.dedup();
180 Ok(Self {
181 whole_area_square_metres,
182 covered_area_square_metres: covered_area_square_metres + 0.0,
186 elements,
187 })
188 }
189 pub fn whole_area_square_metres(&self) -> f64 {
190 self.whole_area_square_metres
191 }
192 pub fn covered_area_square_metres(&self) -> f64 {
193 self.covered_area_square_metres
194 }
195 pub fn elements(&self) -> &[ObjectId] {
196 &self.elements
197 }
198 pub fn covered_ratio(&self) -> f64 {
202 self.covered_area_square_metres / self.whole_area_square_metres
203 }
204}
205
206#[derive(Clone, Debug, PartialEq)]
212pub struct UnallocatedRegion {
213 storey: ObjectId,
214 area_square_metres: f64,
215 elements: Vec<ObjectId>,
216 floor_area_square_metres: Option<f64>,
217}
218
219impl UnallocatedRegion {
220 pub fn try_new(
223 storey: ObjectId,
224 area_square_metres: f64,
225 mut elements: Vec<ObjectId>,
226 ) -> Result<Self, SpaceError> {
227 if !finite_non_negative(area_square_metres) {
228 return Err(SpaceError::InvalidQuantity);
229 }
230 elements.sort();
231 elements.dedup();
232 Ok(Self {
233 storey,
234 area_square_metres,
235 elements,
236 floor_area_square_metres: None,
237 })
238 }
239 pub fn with_floor_area(mut self, square_metres: f64) -> Result<Self, SpaceError> {
243 if !finite_non_negative(square_metres) || square_metres < self.area_square_metres {
244 return Err(SpaceError::InvalidQuantity);
245 }
246 self.floor_area_square_metres = Some(square_metres);
247 Ok(self)
248 }
249 pub fn storey(&self) -> &ObjectId {
250 &self.storey
251 }
252 pub fn area_square_metres(&self) -> f64 {
253 self.area_square_metres
254 }
255 pub fn floor_area_square_metres(&self) -> Option<f64> {
257 self.floor_area_square_metres
258 }
259 pub fn elements(&self) -> &[ObjectId] {
261 &self.elements
262 }
263}
264
265#[derive(Clone, Debug, PartialEq)]
270pub struct SupportCounts {
271 slabs: usize,
272 roofs: usize,
273 buildings: Vec<ObjectId>,
274}
275
276impl SupportCounts {
277 pub fn new(slabs: usize, roofs: usize, mut buildings: Vec<ObjectId>) -> Self {
278 buildings.sort();
279 buildings.dedup();
280 Self {
281 slabs,
282 roofs,
283 buildings,
284 }
285 }
286 pub fn slabs(&self) -> usize {
287 self.slabs
288 }
289 pub fn roofs(&self) -> usize {
290 self.roofs
291 }
292 pub fn buildings(&self) -> &[ObjectId] {
293 &self.buildings
294 }
295}
296
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299pub enum Cap {
300 Top,
301 Bottom,
302}
303
304#[derive(Clone, Debug, PartialEq, Eq)]
312pub struct CapRequest {
313 cap: Cap,
314 elements: Option<Vec<ObjectId>>,
315}
316
317impl CapRequest {
318 #[must_use]
320 pub fn new(cap: Cap) -> Self {
321 Self {
322 cap,
323 elements: None,
324 }
325 }
326 #[must_use]
328 pub fn with_elements(mut self, mut elements: Vec<ObjectId>) -> Self {
329 elements.sort();
330 elements.dedup();
331 self.elements = Some(elements);
332 self
333 }
334 pub fn cap(&self) -> Cap {
335 self.cap
336 }
337 pub fn elements(&self) -> Option<&[ObjectId]> {
339 self.elements.as_deref()
340 }
341}
342
343#[derive(Clone, Debug, Default, PartialEq, Eq)]
350pub struct BoundaryRequest {
351 elements: Option<Vec<ObjectId>>,
352}
353
354impl BoundaryRequest {
355 #[must_use]
357 pub fn new() -> Self {
358 Self::default()
359 }
360 #[must_use]
362 pub fn with_elements(mut self, elements: Vec<ObjectId>) -> Self {
363 self.elements = Some(canonical(elements));
364 self
365 }
366 pub fn elements(&self) -> Option<&[ObjectId]> {
368 self.elements.as_deref()
369 }
370}
371
372#[derive(Clone, Debug, Default, PartialEq, Eq)]
379pub struct OverlapRequest {
380 elements: Option<Vec<ObjectId>>,
381}
382
383impl OverlapRequest {
384 #[must_use]
386 pub fn new() -> Self {
387 Self::default()
388 }
389 #[must_use]
391 pub fn with_elements(mut self, elements: Vec<ObjectId>) -> Self {
392 self.elements = Some(canonical(elements));
393 self
394 }
395 pub fn elements(&self) -> Option<&[ObjectId]> {
397 self.elements.as_deref()
398 }
399}
400
401fn canonical(mut elements: Vec<ObjectId>) -> Vec<ObjectId> {
402 elements.sort();
403 elements.dedup();
404 elements
405}
406
407pub trait SpaceService: Send + Sync + 'static {
412 fn measure_duplicates(&self, space: &ObjectId) -> Result<Vec<ObjectId>, SpaceError>;
414 fn measure_clear_height(&self, space: &ObjectId) -> Result<ClearHeightEvidence, SpaceError>;
416 fn measure_boundary_gaps(
419 &self,
420 space: &ObjectId,
421 request: &BoundaryRequest,
422 ) -> Result<Vec<BoundaryGap>, SpaceError>;
423 fn measure_overlaps(
426 &self,
427 space: &ObjectId,
428 request: &OverlapRequest,
429 ) -> Result<Vec<SpaceOverlap>, SpaceError>;
430 fn measure_cap_coverage(
433 &self,
434 space: &ObjectId,
435 request: &CapRequest,
436 ) -> Result<CapCoverage, SpaceError>;
437 fn measure_unallocated_regions(&self) -> Result<Vec<UnallocatedRegion>, SpaceError>;
440 fn measure_support_counts(&self) -> Result<SupportCounts, SpaceError>;
442 fn evidence(&self) -> Evidence;
444}
445
446#[derive(Clone)]
448pub struct SpaceServiceHandle(Arc<dyn SpaceService>);
449
450impl SpaceServiceHandle {
451 pub fn new(service: Arc<dyn SpaceService>) -> Self {
452 Self(service)
453 }
454 pub fn get(&self) -> &dyn SpaceService {
455 self.0.as_ref()
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use axioval_ir::SourceId;
463
464 fn source() -> SourceId {
465 SourceId::new("cad", "m").unwrap()
466 }
467 fn oid(local: &str) -> ObjectId {
468 ObjectId::new(source(), local).unwrap()
469 }
470
471 #[test]
472 fn cap_covered_over_its_own_area_is_refused() {
473 assert_eq!(
474 CapCoverage::try_new(10.0, 11.0, Vec::new()),
475 Err(SpaceError::InvalidQuantity)
476 );
477 }
478
479 #[test]
480 fn zero_cap_area_is_refused_so_the_ratio_cannot_divide_by_zero() {
481 assert_eq!(
482 CapCoverage::try_new(0.0, 0.0, Vec::new()),
483 Err(SpaceError::InvalidQuantity)
484 );
485 }
486
487 #[test]
491 fn negative_zero_coverage_is_normalised() {
492 let coverage = CapCoverage::try_new(10.0, -0.0, Vec::new()).unwrap();
493 assert_eq!(format!("{:.1}", coverage.covered_ratio() * 100.0), "0.0");
494 }
495
496 #[test]
497 fn cap_request_elements_are_canonical_and_absent_by_default() {
498 assert_eq!(CapRequest::new(Cap::Top).elements(), None);
499 let request =
500 CapRequest::new(Cap::Bottom).with_elements(vec![oid("b"), oid("a"), oid("b")]);
501 assert_eq!(request.cap(), Cap::Bottom);
502 assert_eq!(request.elements(), Some(&[oid("a"), oid("b")][..]));
503 assert_eq!(
505 CapRequest::new(Cap::Top)
506 .with_elements(Vec::new())
507 .elements(),
508 Some(&[][..])
509 );
510 }
511
512 #[test]
513 fn boundary_and_overlap_requests_are_canonical_and_absent_by_default() {
514 assert_eq!(BoundaryRequest::new().elements(), None);
515 assert_eq!(OverlapRequest::new().elements(), None);
516 let ids = vec![oid("b"), oid("a"), oid("b")];
517 assert_eq!(
518 BoundaryRequest::new().with_elements(ids.clone()).elements(),
519 Some(&[oid("a"), oid("b")][..])
520 );
521 assert_eq!(
522 OverlapRequest::new().with_elements(ids).elements(),
523 Some(&[oid("a"), oid("b")][..])
524 );
525 }
526
527 #[test]
528 fn cap_ratio_is_exact() {
529 let coverage = CapCoverage::try_new(4.0, 1.0, Vec::new()).unwrap();
530 assert!((coverage.covered_ratio() - 0.25).abs() < f64::EPSILON);
531 }
532
533 #[test]
534 fn element_lists_are_normalised() {
535 let gap = BoundaryGap::try_new(1.0, vec![oid("w2"), oid("w1"), oid("w2")]).unwrap();
536 assert_eq!(gap.elements(), &[oid("w1"), oid("w2")]);
537 }
538
539 #[test]
540 fn non_finite_quantities_are_refused() {
541 assert!(
542 ClearHeightEvidence::try_new(oid("s"), f64::NAN, Evidence::exact(source(), "h"))
543 .is_err()
544 );
545 assert!(BoundaryGap::try_new(f64::INFINITY, Vec::new()).is_err());
546 assert!(SpaceOverlap::try_new(oid("o"), false, -1.0, 1.0, Containment::Partial).is_err());
547 assert!(UnallocatedRegion::try_new(oid("st"), f64::NAN, Vec::new()).is_err());
548 let region = UnallocatedRegion::try_new(oid("st"), 5.0, Vec::new()).unwrap();
549 assert_eq!(region.floor_area_square_metres(), None);
550 assert!(region.clone().with_floor_area(4.0).is_err());
552 assert!(region.clone().with_floor_area(f64::INFINITY).is_err());
553 assert_eq!(
554 region
555 .with_floor_area(100.0)
556 .unwrap()
557 .floor_area_square_metres(),
558 Some(100.0)
559 );
560 }
561
562 #[test]
563 fn inexact_evidence_is_refused() {
564 assert_eq!(
565 ClearHeightEvidence::try_new(
566 oid("s"),
567 2.5,
568 Evidence {
569 source: source(),
570 locator: "h".into(),
571 exact: false,
572 },
573 ),
574 Err(SpaceError::InexactEvidence)
575 );
576 }
577}