1use std::sync::Arc;
18
19use axioval_ir::{Evidence, ObjectId};
20use thiserror::Error;
21
22use crate::MetricDirection;
23
24#[derive(Clone, Debug, Error, PartialEq, Eq)]
26pub enum VerticalExtentError {
27 #[error("no geometry for `{0}`")]
29 UnknownObject(ObjectId),
30 #[error("vertical extent unavailable: {0}")]
33 Unavailable(String),
34 #[error("vertical extent measurement is invalid")]
37 InvalidMeasurement,
38 #[error("vertical extent evidence does not match its exactness")]
40 InexactEvidence,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct ElevationInterval {
46 lower: f64,
47 upper: f64,
48}
49
50impl ElevationInterval {
51 pub fn try_new(lower: f64, upper: f64) -> Result<Self, VerticalExtentError> {
53 if !lower.is_finite() || !upper.is_finite() || lower > upper {
54 return Err(VerticalExtentError::InvalidMeasurement);
55 }
56 Ok(Self { lower, upper })
57 }
58
59 pub fn exact(metres: f64) -> Result<Self, VerticalExtentError> {
61 Self::try_new(metres, metres)
62 }
63
64 #[must_use]
66 pub fn lower_metres(&self) -> f64 {
67 self.lower
68 }
69
70 #[must_use]
72 pub fn upper_metres(&self) -> f64 {
73 self.upper
74 }
75
76 #[must_use]
78 #[allow(clippy::float_cmp)]
79 pub fn is_exact(&self) -> bool {
80 self.lower == self.upper
81 }
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct VerticalExtent {
87 object: ObjectId,
88 bottom: ElevationInterval,
89 top: ElevationInterval,
90 evidence: Evidence,
91}
92
93impl VerticalExtent {
94 pub fn try_new(
100 object: ObjectId,
101 bottom: ElevationInterval,
102 top: ElevationInterval,
103 evidence: Evidence,
104 ) -> Result<Self, VerticalExtentError> {
105 if bottom.lower > top.lower || bottom.upper > top.upper {
106 return Err(VerticalExtentError::InvalidMeasurement);
107 }
108 let exact = bottom.is_exact() && top.is_exact();
109 if evidence.exact != exact || evidence.locator.trim().is_empty() {
110 return Err(VerticalExtentError::InexactEvidence);
111 }
112 Ok(Self {
113 object,
114 bottom,
115 top,
116 evidence,
117 })
118 }
119
120 #[must_use]
122 pub fn object(&self) -> &ObjectId {
123 &self.object
124 }
125
126 #[must_use]
128 pub fn bottom(&self) -> ElevationInterval {
129 self.bottom
130 }
131
132 #[must_use]
134 pub fn top(&self) -> ElevationInterval {
135 self.top
136 }
137
138 #[must_use]
140 pub fn is_exact(&self) -> bool {
141 self.evidence.exact
142 }
143
144 #[must_use]
146 pub fn evidence(&self) -> &Evidence {
147 &self.evidence
148 }
149
150 #[must_use]
153 pub fn height_metres(&self) -> (f64, f64) {
154 let (low, _) = difference(self.top.lower, self.bottom.upper);
155 let (_, high) = difference(self.top.upper, self.bottom.lower);
156 (low.max(0.0), high.max(0.0))
157 }
158
159 pub fn uncovered_height(
171 &self,
172 cover: &[&VerticalExtent],
173 growth_metres: f64,
174 ) -> Result<(f64, f64), VerticalExtentError> {
175 if !growth_metres.is_finite() || growth_metres < 0.0 {
176 return Err(VerticalExtentError::InvalidMeasurement);
177 }
178 let upper = uncovered_length(
179 (self.bottom.lower, self.top.upper),
180 cover.iter().map(|extent| {
181 (
182 extent.bottom.upper - growth_metres,
183 extent.top.lower + growth_metres,
184 )
185 }),
186 true,
187 );
188 let lower = uncovered_length(
189 (self.bottom.upper, self.top.lower),
190 cover.iter().map(|extent| {
191 (
192 extent.bottom.lower - growth_metres,
193 extent.top.upper + growth_metres,
194 )
195 }),
196 false,
197 );
198 Ok((lower.min(upper), upper))
199 }
200}
201
202fn uncovered_length(span: (f64, f64), cover: impl Iterator<Item = (f64, f64)>, upper: bool) -> f64 {
205 let (start, end) = span;
206 if start >= end {
207 return 0.0;
208 }
209 let mut clipped: Vec<(f64, f64)> = cover
210 .map(|(low, high)| (low.max(start), high.min(end)))
211 .filter(|(low, high)| low < high)
212 .collect();
213 clipped.sort_by(|a, b| a.0.total_cmp(&b.0));
214 let gap = |from: f64, to: f64| {
215 let (low, high) = difference(to, from);
216 if upper { high } else { low }.max(0.0)
217 };
218 let mut cursor = start;
219 let mut uncovered = 0.0;
220 for (low, high) in clipped {
221 if low > cursor {
222 uncovered += gap(cursor, low);
223 }
224 cursor = cursor.max(high);
225 }
226 if cursor < end {
227 uncovered += gap(cursor, end);
228 }
229 uncovered
230}
231
232#[derive(Clone, Debug, PartialEq)]
238pub struct DirectionalExtent {
239 object: ObjectId,
240 direction: MetricDirection,
241 lower: ElevationInterval,
242 upper: ElevationInterval,
243 evidence: Evidence,
244}
245
246impl DirectionalExtent {
247 pub fn try_new(
253 object: ObjectId,
254 direction: MetricDirection,
255 lower: ElevationInterval,
256 upper: ElevationInterval,
257 evidence: Evidence,
258 ) -> Result<Self, VerticalExtentError> {
259 if lower.lower > upper.lower || lower.upper > upper.upper {
260 return Err(VerticalExtentError::InvalidMeasurement);
261 }
262 let exact = lower.is_exact() && upper.is_exact();
263 if evidence.exact != exact || evidence.locator.trim().is_empty() {
264 return Err(VerticalExtentError::InexactEvidence);
265 }
266 Ok(Self {
267 object,
268 direction,
269 lower,
270 upper,
271 evidence,
272 })
273 }
274
275 #[must_use]
277 pub fn object(&self) -> &ObjectId {
278 &self.object
279 }
280
281 #[must_use]
283 pub fn direction(&self) -> MetricDirection {
284 self.direction
285 }
286
287 #[must_use]
289 pub fn lower(&self) -> ElevationInterval {
290 self.lower
291 }
292
293 #[must_use]
295 pub fn upper(&self) -> ElevationInterval {
296 self.upper
297 }
298
299 #[must_use]
302 pub fn length_metres(&self) -> (f64, f64) {
303 let (low, _) = difference(self.upper.lower, self.lower.upper);
304 let (_, high) = difference(self.upper.upper, self.lower.lower);
305 (low.max(0.0), high.max(0.0))
306 }
307
308 #[must_use]
310 pub fn is_exact(&self) -> bool {
311 self.evidence.exact
312 }
313
314 #[must_use]
316 pub fn evidence(&self) -> &Evidence {
317 &self.evidence
318 }
319}
320
321fn difference(minuend: f64, subtrahend: f64) -> (f64, f64) {
324 let rounded = minuend - subtrahend;
325 let back = rounded - minuend;
327 let error = (minuend - (rounded - back)) + (-subtrahend - back);
328 if error > 0.0 {
329 (rounded, rounded.next_up())
330 } else if error < 0.0 {
331 (rounded.next_down(), rounded)
332 } else {
333 (rounded, rounded)
334 }
335}
336
337pub trait VerticalExtentService: Send + Sync + 'static {
339 fn measure_vertical_extent(
341 &self,
342 object: &ObjectId,
343 ) -> Result<VerticalExtent, VerticalExtentError>;
344
345 fn measure_directional_extent(
351 &self,
352 object: &ObjectId,
353 direction: MetricDirection,
354 ) -> Result<DirectionalExtent, VerticalExtentError> {
355 let _ = (object, direction);
356 Err(VerticalExtentError::Unavailable(
357 "this service measures vertical extents only".into(),
358 ))
359 }
360}
361
362#[derive(Clone)]
364pub struct VerticalExtentServiceHandle(Arc<dyn VerticalExtentService>);
365
366impl VerticalExtentServiceHandle {
367 #[must_use]
369 pub fn new(service: Arc<dyn VerticalExtentService>) -> Self {
370 Self(service)
371 }
372
373 pub fn measure_vertical_extent(
376 &self,
377 object: &ObjectId,
378 ) -> Result<VerticalExtent, VerticalExtentError> {
379 let extent = self.0.measure_vertical_extent(object)?;
380 if extent.object() != object {
381 return Err(VerticalExtentError::InvalidMeasurement);
382 }
383 Ok(extent)
384 }
385
386 pub fn measure_directional_extent(
390 &self,
391 object: &ObjectId,
392 direction: MetricDirection,
393 ) -> Result<DirectionalExtent, VerticalExtentError> {
394 let extent = self.0.measure_directional_extent(object, direction)?;
395 if extent.object() != object || extent.direction() != direction {
396 return Err(VerticalExtentError::InvalidMeasurement);
397 }
398 Ok(extent)
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use axioval_ir::SourceId;
406
407 fn id(local: &str) -> ObjectId {
408 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
409 }
410
411 fn exact() -> Evidence {
412 Evidence::exact(SourceId::new("cad", "m").unwrap(), "vertical-extent:a")
413 }
414
415 fn point(value: f64) -> ElevationInterval {
416 ElevationInterval::exact(value).unwrap()
417 }
418
419 #[test]
420 fn exactness_and_intervals_must_agree() {
421 assert!(VerticalExtent::try_new(id("a"), point(0.0), point(0.2), exact()).is_ok());
422 let widened = ElevationInterval::try_new(0.199, 0.201).unwrap();
423 assert_eq!(
424 VerticalExtent::try_new(id("a"), point(0.0), widened, exact()),
425 Err(VerticalExtentError::InexactEvidence)
426 );
427 let mut approximate = exact();
428 approximate.exact = false;
429 assert!(VerticalExtent::try_new(id("a"), point(0.0), widened, approximate.clone()).is_ok());
430 assert_eq!(
431 VerticalExtent::try_new(id("a"), point(0.0), point(0.2), approximate),
432 Err(VerticalExtentError::InexactEvidence)
433 );
434 }
435
436 #[test]
437 fn incoherent_elevations_are_refused() {
438 assert_eq!(
439 ElevationInterval::try_new(1.0, 0.0),
440 Err(VerticalExtentError::InvalidMeasurement)
441 );
442 assert_eq!(
443 ElevationInterval::try_new(f64::NAN, 0.0),
444 Err(VerticalExtentError::InvalidMeasurement)
445 );
446 assert_eq!(
447 VerticalExtent::try_new(id("a"), point(1.0), point(0.0), exact()),
448 Err(VerticalExtentError::InvalidMeasurement)
449 );
450 }
451
452 struct Other;
453 impl VerticalExtentService for Other {
454 fn measure_vertical_extent(
455 &self,
456 _: &ObjectId,
457 ) -> Result<VerticalExtent, VerticalExtentError> {
458 VerticalExtent::try_new(id("b"), point(0.0), point(1.0), exact())
459 }
460 }
461
462 fn extent(bottom: (f64, f64), top: (f64, f64)) -> VerticalExtent {
463 let mut evidence = exact();
464 #[allow(clippy::float_cmp)]
465 {
466 evidence.exact = bottom.0 == bottom.1 && top.0 == top.1;
467 }
468 VerticalExtent::try_new(
469 id("a"),
470 ElevationInterval::try_new(bottom.0, bottom.1).unwrap(),
471 ElevationInterval::try_new(top.0, top.1).unwrap(),
472 evidence,
473 )
474 .unwrap()
475 }
476
477 #[test]
478 #[allow(clippy::float_cmp)]
479 fn the_uncovered_height_is_what_no_grown_cover_spans() {
480 let wall = extent((0.0, 0.0), (3.0, 3.0));
481 assert_eq!(wall.uncovered_height(&[], 0.0).unwrap(), (3.0, 3.0));
483 let low = extent((0.5, 0.5), (2.0, 2.0));
485 let high = extent((1.5, 1.5), (2.75, 2.75));
486 assert_eq!(
487 wall.uncovered_height(&[&high, &low], 0.0).unwrap(),
488 (0.75, 0.75)
489 );
490 assert_eq!(
492 wall.uncovered_height(&[&high, &low], 0.25).unwrap(),
493 (0.25, 0.25)
494 );
495 let above = extent((4.0, 4.0), (5.0, 5.0));
497 assert_eq!(wall.uncovered_height(&[&above], 0.5).unwrap(), (3.0, 3.0));
498 assert!(wall.uncovered_height(&[], -0.1).is_err());
499 assert!(wall.uncovered_height(&[], f64::NAN).is_err());
500 }
501
502 #[test]
503 fn an_uncertain_extent_bounds_its_uncovered_height_from_both_sides() {
504 let wall = extent((-0.01, 0.01), (2.99, 3.01));
505 let cover = extent((-0.01, 0.01), (1.99, 2.01));
506 let (lower, upper) = wall.uncovered_height(&[&cover], 0.0).unwrap();
507 assert!((lower - 0.98).abs() < 1e-9 && (upper - 1.04).abs() < 1e-9);
510 let (short, tall) = wall.height_metres();
511 assert!((short - 2.98).abs() < 1e-9 && (tall - 3.02).abs() < 1e-9);
512 }
513
514 #[test]
515 fn an_extent_of_another_object_is_refused() {
516 let handle = VerticalExtentServiceHandle::new(Arc::new(Other));
517 assert_eq!(
518 handle.measure_vertical_extent(&id("a")),
519 Err(VerticalExtentError::InvalidMeasurement)
520 );
521 }
522
523 fn along(vector: [f64; 3]) -> MetricDirection {
524 MetricDirection::try_new(vector).unwrap()
525 }
526
527 #[test]
528 fn a_service_measuring_elevations_only_refuses_directions() {
529 let handle = VerticalExtentServiceHandle::new(Arc::new(Other));
530 assert!(matches!(
531 handle.measure_directional_extent(&id("a"), along([0.0, 1.0, 0.0])),
532 Err(VerticalExtentError::Unavailable(_))
533 ));
534 }
535
536 #[test]
537 fn directional_exactness_and_order_must_agree() {
538 let y = along([0.0, 1.0, 0.0]);
539 let extent =
540 DirectionalExtent::try_new(id("a"), y, point(0.1), point(0.4), exact()).unwrap();
541 let (low, high) = extent.length_metres();
542 assert!(low <= 0.3 && 0.3 <= high && high - low <= 2.0 * f64::EPSILON);
544 assert_eq!(
545 DirectionalExtent::try_new(id("a"), y, point(0.4), point(0.1), exact()),
546 Err(VerticalExtentError::InvalidMeasurement)
547 );
548 let widened = ElevationInterval::try_new(0.39, 0.41).unwrap();
549 assert_eq!(
550 DirectionalExtent::try_new(id("a"), y, point(0.1), widened, exact()),
551 Err(VerticalExtentError::InexactEvidence)
552 );
553 let exact_length = DirectionalExtent::try_new(id("a"), y, point(1.0), point(1.5), exact())
554 .unwrap()
555 .length_metres();
556 assert_eq!(exact_length, (0.5, 0.5));
557 }
558
559 struct Sideways;
560 impl VerticalExtentService for Sideways {
561 fn measure_vertical_extent(
562 &self,
563 object: &ObjectId,
564 ) -> Result<VerticalExtent, VerticalExtentError> {
565 VerticalExtent::try_new(object.clone(), point(0.0), point(1.0), exact())
566 }
567 fn measure_directional_extent(
568 &self,
569 object: &ObjectId,
570 _: MetricDirection,
571 ) -> Result<DirectionalExtent, VerticalExtentError> {
572 DirectionalExtent::try_new(
573 object.clone(),
574 along([1.0, 0.0, 0.0]),
575 point(0.0),
576 point(1.0),
577 exact(),
578 )
579 }
580 }
581
582 #[test]
583 fn an_extent_along_another_direction_is_refused() {
584 let handle = VerticalExtentServiceHandle::new(Arc::new(Sideways));
585 assert!(
586 handle
587 .measure_directional_extent(&id("a"), along([1.0, 0.0, 0.0]))
588 .is_ok()
589 );
590 assert_eq!(
591 handle.measure_directional_extent(&id("a"), along([0.0, 1.0, 0.0])),
592 Err(VerticalExtentError::InvalidMeasurement)
593 );
594 }
595}