Skip to main content

axioval_engine/
plan_area.rs

1//! Plan-projected areas: an object's footprint, and two footprints' overlap.
2//!
3//! ADR 0004: this seam measures. How much floor area a storey's spaces
4//! cover, or whether a space lies within a fire compartment, is a rule's
5//! judgement over these measurements.
6//!
7//! An area is an interval. A mesh that is the object's exact shape measures
8//! exactly; one that approximates curved faces measures within a bound the
9//! adapter derives from its declared chord deviation, and a rule must decide
10//! from the whole interval, never from its midpoint.
11//!
12//! The uncovered area of a footprint is what remains of it once the union of
13//! other footprints, each grown in plan by a stated length, is taken away:
14//! how much of an architectural wall no structural wall stands under.
15//!
16//! The covered area of a footprint is the union of several sources' effect
17//! areas clipped to it ([`crate::CoverageRequest`]): how much of a room the
18//! devices placed in it reach.
19//!
20//! The uncovered elevation area ([`ElevationRequest`]) is the same question
21//! asked in a vertical plane of the object's own: the object and its cover
22//! projected onto the plane through a stated plan axis and the vertical,
23//! each cover grown by a length along the axis and another in height. It is
24//! how much of a wall's face no structural wall stands behind, where plan
25//! and height checked apart pass a wall under a full-height counterpart on
26//! one half and a half-height one on the other.
27
28use std::sync::Arc;
29
30use axioval_ir::{Evidence, ObjectId};
31use thiserror::Error;
32
33use crate::coverage::{CoverageEvidence, CoverageRequest, check_answer};
34
35/// Failure to measure a plan area.
36#[derive(Clone, Debug, Error, PartialEq, Eq)]
37pub enum PlanAreaError {
38    /// The service holds no geometry for this object.
39    #[error("no geometry for `{0}`")]
40    UnknownObject(ObjectId),
41    /// The geometry could not be measured, for example a mesh that fails its
42    /// health audit or an overlay that cannot be computed.
43    #[error("plan area unavailable: {0}")]
44    Unavailable(String),
45    /// A measurement is non-finite, negative, or its bounds are reversed.
46    #[error("plan area measurement is invalid")]
47    InvalidMeasurement,
48    /// An interval was reported as exact, or evidence as inexact, inconsistently.
49    #[error("plan area evidence does not match its exactness")]
50    InexactEvidence,
51}
52
53/// A measured plan area in square metres, with its evidence.
54#[derive(Clone, Debug, PartialEq)]
55pub struct PlanArea {
56    lower: f64,
57    upper: f64,
58    evidence: Evidence,
59}
60
61impl PlanArea {
62    /// An area known to lie in `[lower, upper]`.
63    ///
64    /// The evidence is exact exactly when the bounds coincide: an interval
65    /// cannot be exact evidence, and a point cannot be approximate.
66    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    /// Smallest area the object can have, in square metres.
83    #[must_use]
84    pub fn lower_square_metres(&self) -> f64 {
85        self.lower
86    }
87
88    /// Largest area the object can have, in square metres.
89    #[must_use]
90    pub fn upper_square_metres(&self) -> f64 {
91        self.upper
92    }
93
94    /// Whether the area is known exactly.
95    #[must_use]
96    pub fn is_exact(&self) -> bool {
97        self.evidence.exact
98    }
99
100    /// Reviewable provenance of the measurement.
101    #[must_use]
102    pub fn evidence(&self) -> &Evidence {
103        &self.evidence
104    }
105}
106
107/// The band between two footprints facing each other along a direction.
108///
109/// It is the convex hull of the two footprints, cut to the positions along
110/// `direction` that both footprints reach: between two parallel walls, the
111/// strip between them over the length they share, walls included. Where
112/// their reaches along the direction do not overlap, the band is empty.
113#[derive(Clone, Debug, PartialEq)]
114pub struct PlanBand {
115    first: ObjectId,
116    second: ObjectId,
117    direction: [f64; 2],
118}
119
120impl PlanBand {
121    /// The band between `first` and `second` along `direction`, a plan
122    /// vector normalised here. The two are ordered, so one band has one
123    /// spelling.
124    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    /// The two objects, in order.
153    #[must_use]
154    pub fn objects(&self) -> [&ObjectId; 2] {
155        [&self.first, &self.second]
156    }
157
158    /// The unit plan direction along which the band is cut.
159    #[must_use]
160    pub fn direction(&self) -> [f64; 2] {
161        self.direction
162    }
163}
164
165/// A request for the area of an object's elevation that its cover leaves
166/// uncovered.
167///
168/// The elevation is the object projected onto the vertical plane through
169/// `axis` (a plan direction, normalised here): positions along the axis
170/// against heights. Only the part of each cover object within the object's
171/// own depth across the axis, widened by `along_growth_metres` on both
172/// sides, is projected; that projection is grown by `along_growth_metres`
173/// along the axis and `vertical_growth_metres` in height.
174///
175/// `frame` objects cover by the frame they form, not by their bodies: the
176/// convex hull of their projected parts (a column at each end and a beam
177/// over them enclose the bay between), grown the same way. An empty frame
178/// adds nothing.
179#[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    /// The request, its cover and frame sorted and without repeats.
191    ///
192    /// Refuses an axis that is not a finite plan direction, a growth that is
193    /// negative or not finite, and an object in its own cover or frame.
194    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    /// The object whose elevation is measured.
237    #[must_use]
238    pub fn object(&self) -> &ObjectId {
239        &self.object
240    }
241
242    /// The unit plan direction the elevation runs along.
243    #[must_use]
244    pub fn axis(&self) -> [f64; 2] {
245        self.axis
246    }
247
248    /// The objects covering by their bodies, sorted.
249    #[must_use]
250    pub fn cover(&self) -> &[ObjectId] {
251        &self.cover
252    }
253
254    /// The objects covering by the frame they form, sorted.
255    #[must_use]
256    pub fn frame(&self) -> &[ObjectId] {
257        &self.frame
258    }
259
260    /// Growth along the axis, and the depth added across it on each side.
261    #[must_use]
262    pub fn along_growth_metres(&self) -> f64 {
263        self.along_growth
264    }
265
266    /// Growth in height.
267    #[must_use]
268    pub fn vertical_growth_metres(&self) -> f64 {
269        self.vertical_growth
270    }
271}
272
273/// The elevation area of an object and the part of it its cover leaves
274/// uncovered, each in square metres as an interval.
275#[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    /// The elevation of `object` with area in `area` and uncovered area in
285    /// `uncovered`, each `(lower, upper)`.
286    ///
287    /// Refuses bounds that are not finite, negative or reversed, an
288    /// uncovered area surely larger than the elevation, and evidence that
289    /// is exact unless both are points (or a point claimed inexact).
290    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    /// The measured object.
316    #[must_use]
317    pub fn object(&self) -> &ObjectId {
318        &self.object
319    }
320
321    /// The elevation's area, `(lower, upper)` square metres.
322    #[must_use]
323    pub fn area_square_metres(&self) -> (f64, f64) {
324        self.area
325    }
326
327    /// The uncovered part of it, `(lower, upper)` square metres.
328    #[must_use]
329    pub fn uncovered_square_metres(&self) -> (f64, f64) {
330        self.uncovered
331    }
332
333    /// Whether both areas are known exactly.
334    #[must_use]
335    pub fn is_exact(&self) -> bool {
336        self.evidence.exact
337    }
338
339    /// Reviewable provenance of the measurement.
340    #[must_use]
341    pub fn evidence(&self) -> &Evidence {
342        &self.evidence
343    }
344}
345
346/// Measures plan-projected areas of model objects.
347pub trait PlanAreaService: Send + Sync + 'static {
348    /// The area of `object`'s footprint: its geometry projected onto the
349    /// horizontal plane, overlapping parts counted once.
350    fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError>;
351    /// The area where the footprints of `first` and `second` overlap.
352    fn measure_plan_overlap(
353        &self,
354        first: &ObjectId,
355        second: &ObjectId,
356    ) -> Result<PlanArea, PlanAreaError>;
357
358    /// The area of `object`'s footprint outside the union of the footprints
359    /// of `cover`, each grown by `growth_metres` in every plan direction (the
360    /// set of points within that distance of it).
361    ///
362    /// An empty `cover` leaves the whole footprint uncovered. A service that
363    /// does not measure uncovered areas refuses; it never answers with the
364    /// footprint or with zero.
365    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    /// The area of `object`'s footprint outside the union of `bands`.
378    ///
379    /// An empty set of bands leaves the whole footprint outside. A service
380    /// that does not measure bands refuses; it never answers with the
381    /// footprint or with zero.
382    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    /// How much of the request's subject footprint the union of its
394    /// sources' effect areas covers.
395    ///
396    /// A service that does not measure coverage refuses; it never answers
397    /// with an empty or a whole cover.
398    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    /// The elevation of the request's object and the part of it the
409    /// request's cover and frame, grown, leave uncovered.
410    ///
411    /// A service that does not measure elevations refuses; it never answers
412    /// with the whole elevation or with zero.
413    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/// Registry handle for a [`PlanAreaService`].
425#[derive(Clone)]
426pub struct PlanAreaServiceHandle(Arc<dyn PlanAreaService>);
427
428impl PlanAreaServiceHandle {
429    /// Wraps a trusted plan-area service.
430    #[must_use]
431    pub fn new(service: Arc<dyn PlanAreaService>) -> Self {
432        Self(service)
433    }
434
435    /// The footprint area of `object`.
436    pub fn measure_footprint(&self, object: &ObjectId) -> Result<PlanArea, PlanAreaError> {
437        self.0.measure_footprint(object)
438    }
439
440    /// The overlap of two footprints, never larger than either footprint's
441    /// upper bound would allow; a larger answer is refused.
442    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    /// The area of `object`'s footprint that the footprints of `cover`, grown
451    /// by `growth_metres`, leave uncovered.
452    ///
453    /// A growth that is negative or not finite, or an object covering itself,
454    /// is refused rather than measured; `cover` reaches the service sorted
455    /// and without repeats.
456    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    /// The area of `object`'s footprint outside the union of `bands`.
479    ///
480    /// A band bounded by `object` itself is refused rather than measured;
481    /// `bands` reach the service sorted and without repeats.
482    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    /// How much of the request's subject footprint its sources cover.
510    ///
511    /// An answer about another subject, or not listing exactly the
512    /// requested sources in order, is refused.
513    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    /// The elevation of the request's object and its uncovered part.
523    ///
524    /// An answer about another object is refused.
525    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    /// Measures only footprints, and records the cover it was asked about.
545    #[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}