axioval_engine/
facade_area.rs1use std::sync::Arc;
17
18use axioval_ir::{Evidence, ObjectId};
19use thiserror::Error;
20
21#[derive(Clone, Debug, Error, PartialEq, Eq)]
23pub enum FacadeAreaError {
24 #[error("no geometry for `{0}`")]
26 UnknownObject(ObjectId),
27 #[error("facade area unavailable: {0}")]
31 Unavailable(String),
32 #[error("facade area measurement is invalid")]
35 InvalidMeasurement,
36 #[error("facade area evidence does not match its exactness")]
38 InexactEvidence,
39}
40
41#[derive(Clone, Debug, PartialEq)]
43pub struct FacadeArea {
44 object: ObjectId,
45 lower: f64,
46 upper: f64,
47 evidence: Evidence,
48}
49
50impl FacadeArea {
51 pub fn try_new(
56 object: ObjectId,
57 lower: f64,
58 upper: f64,
59 evidence: Evidence,
60 ) -> Result<Self, FacadeAreaError> {
61 if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
62 return Err(FacadeAreaError::InvalidMeasurement);
63 }
64 #[allow(clippy::float_cmp)]
65 let exact = lower == upper;
66 if evidence.exact != exact || evidence.locator.trim().is_empty() {
67 return Err(FacadeAreaError::InexactEvidence);
68 }
69 Ok(Self {
70 object,
71 lower,
72 upper,
73 evidence,
74 })
75 }
76
77 #[must_use]
79 pub fn object(&self) -> &ObjectId {
80 &self.object
81 }
82
83 #[must_use]
85 pub fn lower_square_metres(&self) -> f64 {
86 self.lower
87 }
88
89 #[must_use]
91 pub fn upper_square_metres(&self) -> f64 {
92 self.upper
93 }
94
95 #[must_use]
97 pub fn is_exact(&self) -> bool {
98 self.evidence.exact
99 }
100
101 #[must_use]
103 pub fn evidence(&self) -> &Evidence {
104 &self.evidence
105 }
106}
107
108pub trait FacadeAreaService: Send + Sync + 'static {
110 fn measure_facade_area(&self, object: &ObjectId) -> Result<FacadeArea, FacadeAreaError>;
112 fn measure_face_area(&self, object: &ObjectId) -> Result<FacadeArea, FacadeAreaError> {
119 let _ = object;
120 Err(FacadeAreaError::Unavailable(
121 "face areas are not measured by this service".into(),
122 ))
123 }
124}
125
126#[derive(Clone)]
128pub struct FacadeAreaServiceHandle(Arc<dyn FacadeAreaService>);
129
130impl FacadeAreaServiceHandle {
131 #[must_use]
133 pub fn new(service: Arc<dyn FacadeAreaService>) -> Self {
134 Self(service)
135 }
136
137 pub fn measure_facade_area(&self, object: &ObjectId) -> Result<FacadeArea, FacadeAreaError> {
140 let area = self.0.measure_facade_area(object)?;
141 if area.object() != object {
142 return Err(FacadeAreaError::InvalidMeasurement);
143 }
144 Ok(area)
145 }
146
147 pub fn measure_face_area(&self, object: &ObjectId) -> Result<FacadeArea, FacadeAreaError> {
150 let area = self.0.measure_face_area(object)?;
151 if area.object() != object {
152 return Err(FacadeAreaError::InvalidMeasurement);
153 }
154 Ok(area)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use axioval_ir::SourceId;
162
163 fn id(local: &str) -> ObjectId {
164 ObjectId::new(SourceId::new("cad", "m").unwrap(), local).unwrap()
165 }
166
167 fn exact() -> Evidence {
168 Evidence::exact(SourceId::new("cad", "m").unwrap(), "facade-area:a")
169 }
170
171 #[test]
172 fn exactness_and_bounds_must_agree() {
173 assert!(FacadeArea::try_new(id("a"), 2.0, 2.0, exact()).is_ok());
174 assert_eq!(
175 FacadeArea::try_new(id("a"), 1.0, 2.0, exact()),
176 Err(FacadeAreaError::InexactEvidence)
177 );
178 let mut approximate = exact();
179 approximate.exact = false;
180 assert!(FacadeArea::try_new(id("a"), 1.0, 2.0, approximate.clone()).is_ok());
181 assert_eq!(
182 FacadeArea::try_new(id("a"), 2.0, 2.0, approximate),
183 Err(FacadeAreaError::InexactEvidence)
184 );
185 }
186
187 #[test]
188 fn reversed_negative_or_non_finite_bounds_are_refused() {
189 for (lower, upper) in [
190 (2.0, 1.0),
191 (-1.0, 1.0),
192 (0.0, f64::NAN),
193 (0.0, f64::INFINITY),
194 ] {
195 assert_eq!(
196 FacadeArea::try_new(id("a"), lower, upper, exact()),
197 Err(FacadeAreaError::InvalidMeasurement),
198 "{lower} {upper}"
199 );
200 }
201 }
202
203 struct Other;
204 impl FacadeAreaService for Other {
205 fn measure_facade_area(&self, _: &ObjectId) -> Result<FacadeArea, FacadeAreaError> {
206 FacadeArea::try_new(id("b"), 1.0, 1.0, exact())
207 }
208 }
209
210 #[test]
211 fn an_area_of_another_object_is_refused() {
212 let handle = FacadeAreaServiceHandle::new(Arc::new(Other));
213 assert_eq!(
214 handle.measure_facade_area(&id("a")),
215 Err(FacadeAreaError::InvalidMeasurement)
216 );
217 }
218}