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,
183 elements,
184 })
185 }
186 pub fn whole_area_square_metres(&self) -> f64 {
187 self.whole_area_square_metres
188 }
189 pub fn covered_area_square_metres(&self) -> f64 {
190 self.covered_area_square_metres
191 }
192 pub fn elements(&self) -> &[ObjectId] {
193 &self.elements
194 }
195 pub fn covered_ratio(&self) -> f64 {
199 self.covered_area_square_metres / self.whole_area_square_metres
200 }
201}
202
203#[derive(Clone, Debug, PartialEq)]
205pub struct StoreyResidual {
206 storey: ObjectId,
207 area_square_metres: f64,
208 elements: Vec<ObjectId>,
209}
210
211impl StoreyResidual {
212 pub fn try_new(
213 storey: ObjectId,
214 area_square_metres: f64,
215 mut elements: Vec<ObjectId>,
216 ) -> Result<Self, SpaceError> {
217 if !finite_non_negative(area_square_metres) {
218 return Err(SpaceError::InvalidQuantity);
219 }
220 elements.sort();
221 elements.dedup();
222 Ok(Self {
223 storey,
224 area_square_metres,
225 elements,
226 })
227 }
228 pub fn storey(&self) -> &ObjectId {
229 &self.storey
230 }
231 pub fn area_square_metres(&self) -> f64 {
232 self.area_square_metres
233 }
234 pub fn elements(&self) -> &[ObjectId] {
235 &self.elements
236 }
237}
238
239#[derive(Clone, Debug, PartialEq)]
244pub struct SupportCounts {
245 slabs: usize,
246 roofs: usize,
247 buildings: Vec<ObjectId>,
248}
249
250impl SupportCounts {
251 pub fn new(slabs: usize, roofs: usize, mut buildings: Vec<ObjectId>) -> Self {
252 buildings.sort();
253 buildings.dedup();
254 Self {
255 slabs,
256 roofs,
257 buildings,
258 }
259 }
260 pub fn slabs(&self) -> usize {
261 self.slabs
262 }
263 pub fn roofs(&self) -> usize {
264 self.roofs
265 }
266 pub fn buildings(&self) -> &[ObjectId] {
267 &self.buildings
268 }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub enum Cap {
274 Top,
275 Bottom,
276}
277
278pub trait SpaceService: Send + Sync + 'static {
283 fn measure_duplicates(&self, space: &ObjectId) -> Result<Vec<ObjectId>, SpaceError>;
285 fn measure_clear_height(&self, space: &ObjectId) -> Result<ClearHeightEvidence, SpaceError>;
287 fn measure_boundary_gaps(&self, space: &ObjectId) -> Result<Vec<BoundaryGap>, SpaceError>;
289 fn measure_overlaps(&self, space: &ObjectId) -> Result<Vec<SpaceOverlap>, SpaceError>;
291 fn measure_cap_coverage(&self, space: &ObjectId, cap: Cap) -> Result<CapCoverage, SpaceError>;
293 fn measure_storey_residuals(&self) -> Result<Vec<StoreyResidual>, SpaceError>;
295 fn measure_support_counts(&self) -> Result<SupportCounts, SpaceError>;
297 fn evidence(&self) -> Evidence;
299}
300
301#[derive(Clone)]
303pub struct SpaceServiceHandle(Arc<dyn SpaceService>);
304
305impl SpaceServiceHandle {
306 pub fn new(service: Arc<dyn SpaceService>) -> Self {
307 Self(service)
308 }
309 pub fn get(&self) -> &dyn SpaceService {
310 self.0.as_ref()
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use axioval_ir::SourceId;
318
319 fn source() -> SourceId {
320 SourceId::new("cad", "m").unwrap()
321 }
322 fn oid(local: &str) -> ObjectId {
323 ObjectId::new(source(), local).unwrap()
324 }
325
326 #[test]
327 fn cap_covered_over_its_own_area_is_refused() {
328 assert_eq!(
329 CapCoverage::try_new(10.0, 11.0, Vec::new()),
330 Err(SpaceError::InvalidQuantity)
331 );
332 }
333
334 #[test]
335 fn zero_cap_area_is_refused_so_the_ratio_cannot_divide_by_zero() {
336 assert_eq!(
337 CapCoverage::try_new(0.0, 0.0, Vec::new()),
338 Err(SpaceError::InvalidQuantity)
339 );
340 }
341
342 #[test]
343 fn cap_ratio_is_exact() {
344 let coverage = CapCoverage::try_new(4.0, 1.0, Vec::new()).unwrap();
345 assert!((coverage.covered_ratio() - 0.25).abs() < f64::EPSILON);
346 }
347
348 #[test]
349 fn element_lists_are_normalised() {
350 let gap = BoundaryGap::try_new(1.0, vec![oid("w2"), oid("w1"), oid("w2")]).unwrap();
351 assert_eq!(gap.elements(), &[oid("w1"), oid("w2")]);
352 }
353
354 #[test]
355 fn non_finite_quantities_are_refused() {
356 assert!(
357 ClearHeightEvidence::try_new(oid("s"), f64::NAN, Evidence::exact(source(), "h"))
358 .is_err()
359 );
360 assert!(BoundaryGap::try_new(f64::INFINITY, Vec::new()).is_err());
361 assert!(SpaceOverlap::try_new(oid("o"), false, -1.0, 1.0, Containment::Partial).is_err());
362 assert!(StoreyResidual::try_new(oid("st"), f64::NAN, Vec::new()).is_err());
363 }
364
365 #[test]
366 fn inexact_evidence_is_refused() {
367 assert_eq!(
368 ClearHeightEvidence::try_new(
369 oid("s"),
370 2.5,
371 Evidence {
372 source: source(),
373 locator: "h".into(),
374 exact: false,
375 },
376 ),
377 Err(SpaceError::InexactEvidence)
378 );
379 }
380}