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)]
208pub struct StoreyResidual {
209 storey: ObjectId,
210 area_square_metres: f64,
211 elements: Vec<ObjectId>,
212}
213
214impl StoreyResidual {
215 pub fn try_new(
216 storey: ObjectId,
217 area_square_metres: f64,
218 mut elements: Vec<ObjectId>,
219 ) -> Result<Self, SpaceError> {
220 if !finite_non_negative(area_square_metres) {
221 return Err(SpaceError::InvalidQuantity);
222 }
223 elements.sort();
224 elements.dedup();
225 Ok(Self {
226 storey,
227 area_square_metres,
228 elements,
229 })
230 }
231 pub fn storey(&self) -> &ObjectId {
232 &self.storey
233 }
234 pub fn area_square_metres(&self) -> f64 {
235 self.area_square_metres
236 }
237 pub fn elements(&self) -> &[ObjectId] {
238 &self.elements
239 }
240}
241
242#[derive(Clone, Debug, PartialEq)]
247pub struct SupportCounts {
248 slabs: usize,
249 roofs: usize,
250 buildings: Vec<ObjectId>,
251}
252
253impl SupportCounts {
254 pub fn new(slabs: usize, roofs: usize, mut buildings: Vec<ObjectId>) -> Self {
255 buildings.sort();
256 buildings.dedup();
257 Self {
258 slabs,
259 roofs,
260 buildings,
261 }
262 }
263 pub fn slabs(&self) -> usize {
264 self.slabs
265 }
266 pub fn roofs(&self) -> usize {
267 self.roofs
268 }
269 pub fn buildings(&self) -> &[ObjectId] {
270 &self.buildings
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum Cap {
277 Top,
278 Bottom,
279}
280
281pub trait SpaceService: Send + Sync + 'static {
286 fn measure_duplicates(&self, space: &ObjectId) -> Result<Vec<ObjectId>, SpaceError>;
288 fn measure_clear_height(&self, space: &ObjectId) -> Result<ClearHeightEvidence, SpaceError>;
290 fn measure_boundary_gaps(&self, space: &ObjectId) -> Result<Vec<BoundaryGap>, SpaceError>;
292 fn measure_overlaps(&self, space: &ObjectId) -> Result<Vec<SpaceOverlap>, SpaceError>;
294 fn measure_cap_coverage(&self, space: &ObjectId, cap: Cap) -> Result<CapCoverage, SpaceError>;
296 fn measure_storey_residuals(&self) -> Result<Vec<StoreyResidual>, SpaceError>;
298 fn measure_support_counts(&self) -> Result<SupportCounts, SpaceError>;
300 fn evidence(&self) -> Evidence;
302}
303
304#[derive(Clone)]
306pub struct SpaceServiceHandle(Arc<dyn SpaceService>);
307
308impl SpaceServiceHandle {
309 pub fn new(service: Arc<dyn SpaceService>) -> Self {
310 Self(service)
311 }
312 pub fn get(&self) -> &dyn SpaceService {
313 self.0.as_ref()
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use axioval_ir::SourceId;
321
322 fn source() -> SourceId {
323 SourceId::new("cad", "m").unwrap()
324 }
325 fn oid(local: &str) -> ObjectId {
326 ObjectId::new(source(), local).unwrap()
327 }
328
329 #[test]
330 fn cap_covered_over_its_own_area_is_refused() {
331 assert_eq!(
332 CapCoverage::try_new(10.0, 11.0, Vec::new()),
333 Err(SpaceError::InvalidQuantity)
334 );
335 }
336
337 #[test]
338 fn zero_cap_area_is_refused_so_the_ratio_cannot_divide_by_zero() {
339 assert_eq!(
340 CapCoverage::try_new(0.0, 0.0, Vec::new()),
341 Err(SpaceError::InvalidQuantity)
342 );
343 }
344
345 #[test]
349 fn negative_zero_coverage_is_normalised() {
350 let coverage = CapCoverage::try_new(10.0, -0.0, Vec::new()).unwrap();
351 assert_eq!(format!("{:.1}", coverage.covered_ratio() * 100.0), "0.0");
352 }
353
354 #[test]
355 fn cap_ratio_is_exact() {
356 let coverage = CapCoverage::try_new(4.0, 1.0, Vec::new()).unwrap();
357 assert!((coverage.covered_ratio() - 0.25).abs() < f64::EPSILON);
358 }
359
360 #[test]
361 fn element_lists_are_normalised() {
362 let gap = BoundaryGap::try_new(1.0, vec![oid("w2"), oid("w1"), oid("w2")]).unwrap();
363 assert_eq!(gap.elements(), &[oid("w1"), oid("w2")]);
364 }
365
366 #[test]
367 fn non_finite_quantities_are_refused() {
368 assert!(
369 ClearHeightEvidence::try_new(oid("s"), f64::NAN, Evidence::exact(source(), "h"))
370 .is_err()
371 );
372 assert!(BoundaryGap::try_new(f64::INFINITY, Vec::new()).is_err());
373 assert!(SpaceOverlap::try_new(oid("o"), false, -1.0, 1.0, Containment::Partial).is_err());
374 assert!(StoreyResidual::try_new(oid("st"), f64::NAN, Vec::new()).is_err());
375 }
376
377 #[test]
378 fn inexact_evidence_is_refused() {
379 assert_eq!(
380 ClearHeightEvidence::try_new(
381 oid("s"),
382 2.5,
383 Evidence {
384 source: source(),
385 locator: "h".into(),
386 exact: false,
387 },
388 ),
389 Err(SpaceError::InexactEvidence)
390 );
391 }
392}