1use std::sync::Arc;
29
30use axioval_ir::{Evidence, ObjectId};
31use thiserror::Error;
32
33use crate::coverage::{CoverageEvidence, CoverageRequest, check_answer};
34
35#[derive(Clone, Debug, Error, PartialEq, Eq)]
37pub enum PlanAreaError {
38 #[error("no geometry for `{0}`")]
40 UnknownObject(ObjectId),
41 #[error("plan area unavailable: {0}")]
44 Unavailable(String),
45 #[error("plan area measurement is invalid")]
47 InvalidMeasurement,
48 #[error("plan area evidence does not match its exactness")]
50 InexactEvidence,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct PlanArea {
56 lower: f64,
57 upper: f64,
58 evidence: Evidence,
59}
60
61impl PlanArea {
62 pub fn try_new(lower: f64, upper: f64, evidence: Evidence) -> Result<Self, PlanAreaError> {
67 if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
68 return Err(PlanAreaError::InvalidMeasurement);
69 }
70 #[allow(clippy::float_cmp)]
71 let exact = lower == upper;
72 if evidence.exact != exact || evidence.locator.trim().is_empty() {
73 return Err(PlanAreaError::InexactEvidence);
74 }
75 Ok(Self {
76 lower,
77 upper,
78 evidence,
79 })
80 }
81
82 #[must_use]
84 pub fn lower_square_metres(&self) -> f64 {
85 self.lower
86 }
87
88 #[must_use]
90 pub fn upper_square_metres(&self) -> f64 {
91 self.upper
92 }
93
94 #[must_use]
96 pub fn is_exact(&self) -> bool {
97 self.evidence.exact
98 }
99
100 #[must_use]
102 pub fn evidence(&self) -> &Evidence {
103 &self.evidence
104 }
105}
106
107#[derive(Clone, Debug, PartialEq)]
114pub struct PlanBand {
115 first: ObjectId,
116 second: ObjectId,
117 direction: [f64; 2],
118}
119
120impl PlanBand {
121 pub fn try_new(
125 first: ObjectId,
126 second: ObjectId,
127 direction: [f64; 2],
128 ) -> Result<Self, PlanAreaError> {
129 let length = direction[0].hypot(direction[1]);
130 if first == second {
131 return Err(PlanAreaError::Unavailable(format!(
132 "a band needs two objects, not {first} twice"
133 )));
134 }
135 if !length.is_finite() || length <= f64::EPSILON {
136 return Err(PlanAreaError::Unavailable(format!(
137 "a band needs a plan direction, not {direction:?}"
138 )));
139 }
140 let (first, second) = if first < second {
141 (first, second)
142 } else {
143 (second, first)
144 };
145 Ok(Self {
146 first,
147 second,
148 direction: [direction[0] / length, direction[1] / length],
149 })
150 }
151
152 #[must_use]
154 pub fn objects(&self) -> [&ObjectId; 2] {
155 [&self.first, &self.second]
156 }
157
158 #[must_use]
160 pub fn direction(&self) -> [f64; 2] {
161 self.direction
162 }
163}
164
165#[derive(Clone, Debug, PartialEq)]
180pub struct ElevationRequest {
181 object: ObjectId,
182 axis: [f64; 2],
183 cover: Vec<ObjectId>,
184 frame: Vec<ObjectId>,
185 along_growth: f64,
186 vertical_growth: f64,
187}
188
189impl ElevationRequest {
190 pub fn try_new(
195 object: ObjectId,
196 axis: [f64; 2],
197 cover: &[ObjectId],
198 frame: &[ObjectId],
199 along_growth_metres: f64,
200 vertical_growth_metres: f64,
201 ) -> Result<Self, PlanAreaError> {
202 let length = axis[0].hypot(axis[1]);
203 if !length.is_finite() || length <= f64::EPSILON {
204 return Err(PlanAreaError::Unavailable(format!(
205 "an elevation needs a plan axis, not {axis:?}"
206 )));
207 }
208 for growth in [along_growth_metres, vertical_growth_metres] {
209 if !growth.is_finite() || growth < 0.0 {
210 return Err(PlanAreaError::Unavailable(format!(
211 "a growth of {growth} m is not a non-negative length"
212 )));
213 }
214 }
215 if cover.contains(&object) || frame.contains(&object) {
216 return Err(PlanAreaError::Unavailable(format!(
217 "{object} cannot cover its own elevation"
218 )));
219 }
220 let sorted = |objects: &[ObjectId]| {
221 let mut objects = objects.to_vec();
222 objects.sort();
223 objects.dedup();
224 objects
225 };
226 Ok(Self {
227 object,
228 axis: [axis[0] / length, axis[1] / length],
229 cover: sorted(cover),
230 frame: sorted(frame),
231 along_growth: along_growth_metres,
232 vertical_growth: vertical_growth_metres,
233 })
234 }
235
236 #[must_use]
238 pub fn object(&self) -> &ObjectId {
239 &self.object
240 }
241
242 #[must_use]
244 pub fn axis(&self) -> [f64; 2] {
245 self.axis
246 }
247
248 #[must_use]
250 pub fn cover(&self) -> &[ObjectId] {
251 &self.cover
252 }
253
254 #[must_use]
256 pub fn frame(&self) -> &[ObjectId] {
257 &self.frame
258 }
259
260 #[must_use]
262 pub fn along_growth_metres(&self) -> f64 {
263 self.along_growth
264 }
265
266 #[must_use]
268 pub fn vertical_growth_metres(&self) -> f64 {
269 self.vertical_growth
270 }
271}
272
273#[derive(Clone, Debug, PartialEq)]
276pub struct ElevationCover {
277 object: ObjectId,
278 area: (f64, f64),
279 uncovered: (f64, f64),
280 evidence: Evidence,
281}
282
283impl ElevationCover {
284 pub fn try_new(
291 object: ObjectId,
292 area: (f64, f64),
293 uncovered: (f64, f64),
294 evidence: Evidence,
295 ) -> Result<Self, PlanAreaError> {
296 let valid = |(lower, upper): (f64, f64)| {
297 lower.is_finite() && upper.is_finite() && lower >= 0.0 && lower <= upper
298 };
299 if !valid(area) || !valid(uncovered) || uncovered.0 > area.1 {
300 return Err(PlanAreaError::InvalidMeasurement);
301 }
302 #[allow(clippy::float_cmp)]
303 let exact = area.0 == area.1 && uncovered.0 == uncovered.1;
304 if evidence.exact != exact || evidence.locator.trim().is_empty() {
305 return Err(PlanAreaError::InexactEvidence);
306 }
307 Ok(Self {
308 object,
309 area,
310 uncovered,
311 evidence,
312 })
313 }
314
315 #[must_use]
317 pub fn object(&self) -> &ObjectId {
318 &self.object
319 }
320
321 #[must_use]
323 pub fn area_square_metres(&self) -> (f64, f64) {
324 self.area
325 }
326
327 #[must_use]
329 pub fn uncovered_square_metres(&self) -> (f64, f64) {
330 self.uncovered
331 }
332
333 #[must_use]
335 pub fn is_exact(&self) -> bool {
336 self.evidence.exact
337 }
338
339 #[must_use]
341 pub fn evidence(&self) -> &Evidence {
342 &self.evidence
343 }
344}
345
346pub trait PlanAreaService: Send + Sync + 'static {
348 fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError>;
351 fn measure_plan_overlap(
353 &self,
354 first: &ObjectId,
355 second: &ObjectId,
356 ) -> Result<PlanArea, PlanAreaError>;
357
358 fn measure_uncovered_area(
366 &self,
367 object: &ObjectId,
368 cover: &[ObjectId],
369 growth_metres: f64,
370 ) -> Result<PlanArea, PlanAreaError> {
371 let _ = (object, cover, growth_metres);
372 Err(PlanAreaError::Unavailable(
373 "this plan-area service does not measure uncovered areas".into(),
374 ))
375 }
376
377 fn measure_outside_bands(
383 &self,
384 object: &ObjectId,
385 bands: &[PlanBand],
386 ) -> Result<PlanArea, PlanAreaError> {
387 let _ = (object, bands);
388 Err(PlanAreaError::Unavailable(
389 "this plan-area service does not measure bands".into(),
390 ))
391 }
392
393 fn measure_coverage(
399 &self,
400 request: &CoverageRequest,
401 ) -> Result<CoverageEvidence, PlanAreaError> {
402 Err(PlanAreaError::Unavailable(format!(
403 "this plan-area service does not measure the coverage of {}",
404 request.subject()
405 )))
406 }
407
408 fn measure_elevation_cover(
414 &self,
415 request: &ElevationRequest,
416 ) -> Result<ElevationCover, PlanAreaError> {
417 Err(PlanAreaError::Unavailable(format!(
418 "this plan-area service does not measure the elevation of {}",
419 request.object()
420 )))
421 }
422}
423
424#[derive(Clone)]
426pub struct PlanAreaServiceHandle(Arc<dyn PlanAreaService>);
427
428impl PlanAreaServiceHandle {
429 #[must_use]
431 pub fn new(service: Arc<dyn PlanAreaService>) -> Self {
432 Self(service)
433 }
434
435 pub fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError> {
437 self.0.measure_footprint(object)
438 }
439
440 pub fn measure_plan_overlap(
443 &self,
444 first: &ObjectId,
445 second: &ObjectId,
446 ) -> Result<PlanArea, PlanAreaError> {
447 self.0.measure_plan_overlap(first, second)
448 }
449
450 pub fn measure_uncovered_area(
457 &self,
458 object: &ObjectId,
459 cover: &[ObjectId],
460 growth_metres: f64,
461 ) -> Result<PlanArea, PlanAreaError> {
462 if !growth_metres.is_finite() || growth_metres < 0.0 {
463 return Err(PlanAreaError::Unavailable(format!(
464 "a growth of {growth_metres} m is not a non-negative length"
465 )));
466 }
467 if cover.contains(object) {
468 return Err(PlanAreaError::Unavailable(format!(
469 "{object} cannot cover its own footprint"
470 )));
471 }
472 let mut cover = cover.to_vec();
473 cover.sort();
474 cover.dedup();
475 self.0.measure_uncovered_area(object, &cover, growth_metres)
476 }
477
478 pub fn measure_outside_bands(
483 &self,
484 object: &ObjectId,
485 bands: &[PlanBand],
486 ) -> Result<PlanArea, PlanAreaError> {
487 if bands.iter().any(|band| band.objects().contains(&object)) {
488 return Err(PlanAreaError::Unavailable(format!(
489 "{object} cannot bound a band over its own footprint"
490 )));
491 }
492 let mut bands = bands.to_vec();
493 bands.sort_by(|a, b| {
494 (
495 a.objects(),
496 a.direction[0].to_bits(),
497 a.direction[1].to_bits(),
498 )
499 .cmp(&(
500 b.objects(),
501 b.direction[0].to_bits(),
502 b.direction[1].to_bits(),
503 ))
504 });
505 bands.dedup();
506 self.0.measure_outside_bands(object, &bands)
507 }
508
509 pub fn measure_coverage(
514 &self,
515 request: &CoverageRequest,
516 ) -> Result<CoverageEvidence, PlanAreaError> {
517 let answer = self.0.measure_coverage(request)?;
518 check_answer(request, &answer)?;
519 Ok(answer)
520 }
521
522 pub fn measure_elevation_cover(
526 &self,
527 request: &ElevationRequest,
528 ) -> Result<ElevationCover, PlanAreaError> {
529 let answer = self.0.measure_elevation_cover(request)?;
530 if answer.object() != request.object() {
531 return Err(PlanAreaError::InvalidMeasurement);
532 }
533 Ok(answer)
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use std::sync::{Arc, Mutex};
540
541 use super::{PlanArea, PlanAreaError, PlanAreaService, PlanAreaServiceHandle};
542 use axioval_ir::{Evidence, ObjectId, SourceId};
543
544 #[derive(Default)]
546 struct FootprintsOnly(Mutex<Vec<Vec<ObjectId>>>);
547
548 impl PlanAreaService for FootprintsOnly {
549 fn measure_footprint(&self, _: &ObjectId) -> Result<PlanArea, PlanAreaError> {
550 PlanArea::try_new(1.0, 1.0, exact())
551 }
552 fn measure_plan_overlap(
553 &self,
554 _: &ObjectId,
555 _: &ObjectId,
556 ) -> Result<PlanArea, PlanAreaError> {
557 PlanArea::try_new(0.0, 0.0, exact())
558 }
559 }
560
561 struct Recording(Arc<FootprintsOnly>);
562
563 impl PlanAreaService for Recording {
564 fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError> {
565 self.0.measure_footprint(object)
566 }
567 fn measure_plan_overlap(
568 &self,
569 first: &ObjectId,
570 second: &ObjectId,
571 ) -> Result<PlanArea, PlanAreaError> {
572 self.0.measure_plan_overlap(first, second)
573 }
574 fn measure_uncovered_area(
575 &self,
576 _: &ObjectId,
577 cover: &[ObjectId],
578 _: f64,
579 ) -> Result<PlanArea, PlanAreaError> {
580 self.0.0.lock().unwrap().push(cover.to_vec());
581 PlanArea::try_new(0.5, 0.5, exact())
582 }
583 }
584
585 fn id(local: &str) -> ObjectId {
586 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
587 }
588
589 #[test]
590 fn a_service_without_uncovered_areas_refuses_rather_than_answering() {
591 let handle = PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()));
592 assert!(matches!(
593 handle.measure_uncovered_area(&id("a"), &[id("b")], 0.0),
594 Err(PlanAreaError::Unavailable(_))
595 ));
596 let band = super::PlanBand::try_new(id("b"), id("c"), [1.0, 0.0]).unwrap();
597 assert!(matches!(
598 handle.measure_outside_bands(&id("a"), &[band]),
599 Err(PlanAreaError::Unavailable(_))
600 ));
601 }
602
603 #[test]
604 #[allow(clippy::float_cmp)]
605 fn a_band_is_ordered_normalised_and_never_bounded_by_its_subject() {
606 let band = super::PlanBand::try_new(id("c"), id("b"), [0.0, 2.0]).unwrap();
607 assert_eq!(band.objects(), [&id("b"), &id("c")]);
608 assert_eq!(band.direction(), [0.0, 1.0]);
609 assert!(super::PlanBand::try_new(id("b"), id("b"), [1.0, 0.0]).is_err());
610 assert!(super::PlanBand::try_new(id("b"), id("c"), [0.0, 0.0]).is_err());
611 assert!(super::PlanBand::try_new(id("b"), id("c"), [f64::NAN, 1.0]).is_err());
612 let handle = PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()));
613 assert!(matches!(
614 handle.measure_outside_bands(&id("b"), &[band]),
615 Err(PlanAreaError::Unavailable(message)) if message.contains("its own footprint")
616 ));
617 }
618
619 #[test]
620 fn the_handle_refuses_a_bad_growth_or_self_cover_and_orders_the_cover() {
621 let log = Arc::new(FootprintsOnly::default());
622 let handle = PlanAreaServiceHandle::new(Arc::new(Recording(log.clone())));
623 for growth in [-0.01, f64::NAN, f64::INFINITY] {
624 assert!(
625 handle
626 .measure_uncovered_area(&id("a"), &[id("b")], growth)
627 .is_err(),
628 "{growth}"
629 );
630 }
631 assert!(
632 handle
633 .measure_uncovered_area(&id("a"), &[id("b"), id("a")], 0.0)
634 .is_err()
635 );
636 assert!(log.0.lock().unwrap().is_empty(), "refused before measuring");
637 handle
638 .measure_uncovered_area(&id("a"), &[id("c"), id("b"), id("c")], 0.1)
639 .unwrap();
640 assert_eq!(*log.0.lock().unwrap(), vec![vec![id("b"), id("c")]]);
641 }
642
643 fn exact() -> Evidence {
644 Evidence::exact(SourceId::new("cad", "m").unwrap(), "footprint:a")
645 }
646
647 #[test]
648 fn exactness_and_bounds_must_agree() {
649 assert!(PlanArea::try_new(2.0, 2.0, exact()).is_ok());
650 assert_eq!(
651 PlanArea::try_new(1.0, 2.0, exact()),
652 Err(PlanAreaError::InexactEvidence)
653 );
654 let mut approximate = exact();
655 approximate.exact = false;
656 assert!(PlanArea::try_new(1.0, 2.0, approximate.clone()).is_ok());
657 assert_eq!(
658 PlanArea::try_new(2.0, 2.0, approximate),
659 Err(PlanAreaError::InexactEvidence)
660 );
661 }
662
663 #[test]
664 fn reversed_negative_or_non_finite_bounds_are_refused() {
665 for (lower, upper) in [
666 (2.0, 1.0),
667 (-1.0, 1.0),
668 (0.0, f64::NAN),
669 (0.0, f64::INFINITY),
670 ] {
671 assert_eq!(
672 PlanArea::try_new(lower, upper, exact()),
673 Err(PlanAreaError::InvalidMeasurement),
674 "{lower} {upper}"
675 );
676 }
677 }
678
679 #[test]
680 #[allow(clippy::float_cmp)]
681 fn an_elevation_request_is_normalised_ordered_and_never_covers_itself() {
682 use super::ElevationRequest;
683 let request = ElevationRequest::try_new(
684 id("a"),
685 [0.0, 2.0],
686 &[id("c"), id("b"), id("c")],
687 &[],
688 0.1,
689 0.2,
690 )
691 .unwrap();
692 assert_eq!(request.axis(), [0.0, 1.0]);
693 assert_eq!(request.cover(), [id("b"), id("c")]);
694 assert!(request.frame().is_empty());
695 assert_eq!(
696 (
697 request.along_growth_metres(),
698 request.vertical_growth_metres()
699 ),
700 (0.1, 0.2)
701 );
702 for (axis, along, vertical) in [
703 ([0.0, 0.0], 0.0, 0.0),
704 ([f64::NAN, 1.0], 0.0, 0.0),
705 ([1.0, 0.0], -0.1, 0.0),
706 ([1.0, 0.0], 0.0, f64::INFINITY),
707 ] {
708 assert!(
709 ElevationRequest::try_new(id("a"), axis, &[], &[], along, vertical).is_err(),
710 "{axis:?} {along} {vertical}"
711 );
712 }
713 assert!(ElevationRequest::try_new(id("a"), [1.0, 0.0], &[id("a")], &[], 0.0, 0.0).is_err());
714 assert!(ElevationRequest::try_new(id("a"), [1.0, 0.0], &[], &[id("a")], 0.0, 0.0).is_err());
715 }
716
717 #[test]
718 fn an_elevation_cover_must_be_coherent_and_about_the_requested_object() {
719 use super::ElevationCover;
720 let mut approximate = exact();
721 approximate.exact = false;
722 assert!(ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.0), exact()).is_ok());
723 assert!(
724 ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.5), approximate.clone()).is_ok()
725 );
726 assert_eq!(
727 ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.5), exact()),
728 Err(PlanAreaError::InexactEvidence)
729 );
730 assert_eq!(
731 ElevationCover::try_new(id("a"), (2.0, 2.0), (1.0, 1.0), approximate.clone()),
732 Err(PlanAreaError::InexactEvidence)
733 );
734 for (area, uncovered) in [
735 ((2.0, 1.0), (0.0, 0.0)),
736 ((1.0, 1.0), (1.5, 1.5)),
737 ((1.0, 1.0), (-0.5, 0.5)),
738 ((1.0, f64::NAN), (0.0, 0.0)),
739 ] {
740 assert_eq!(
741 ElevationCover::try_new(id("a"), area, uncovered, approximate.clone()),
742 Err(PlanAreaError::InvalidMeasurement),
743 "{area:?} {uncovered:?}"
744 );
745 }
746 }
747
748 #[test]
749 fn an_elevation_about_another_object_or_unmeasured_is_refused() {
750 use super::{ElevationCover, ElevationRequest};
751 struct Elsewhere;
752 impl PlanAreaService for Elsewhere {
753 fn measure_footprint(&self, _: &ObjectId) -> Result<PlanArea, PlanAreaError> {
754 PlanArea::try_new(1.0, 1.0, exact())
755 }
756 fn measure_plan_overlap(
757 &self,
758 _: &ObjectId,
759 _: &ObjectId,
760 ) -> Result<PlanArea, PlanAreaError> {
761 PlanArea::try_new(0.0, 0.0, exact())
762 }
763 fn measure_elevation_cover(
764 &self,
765 _: &ElevationRequest,
766 ) -> Result<ElevationCover, PlanAreaError> {
767 ElevationCover::try_new(id("b"), (1.0, 1.0), (0.0, 0.0), exact())
768 }
769 }
770 let request = ElevationRequest::try_new(id("a"), [1.0, 0.0], &[], &[], 0.0, 0.0).unwrap();
771 assert_eq!(
772 PlanAreaServiceHandle::new(Arc::new(Elsewhere)).measure_elevation_cover(&request),
773 Err(PlanAreaError::InvalidMeasurement)
774 );
775 assert!(matches!(
776 PlanAreaServiceHandle::new(Arc::new(FootprintsOnly::default()))
777 .measure_elevation_cover(&request),
778 Err(PlanAreaError::Unavailable(_))
779 ));
780 }
781}