1use std::sync::Arc;
24
25use axioval_ir::{Evidence, ObjectId};
26
27use crate::plan_span::{PlanLength, PlanSpanError, PlanSpanService};
28
29#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct CorridorEndRequest {
33 space: ObjectId,
34 subjects: Vec<ObjectId>,
35}
36
37impl CorridorEndRequest {
38 pub fn try_new(
42 space: ObjectId,
43 subjects: impl IntoIterator<Item = ObjectId>,
44 ) -> Result<Self, PlanSpanError> {
45 let mut subjects: Vec<ObjectId> = subjects.into_iter().collect();
46 subjects.sort();
47 subjects.dedup();
48 if subjects.contains(&space) {
49 return Err(PlanSpanError::Unavailable(format!(
50 "{space} cannot be measured against its own corridor ends"
51 )));
52 }
53 Ok(Self { space, subjects })
54 }
55
56 #[must_use]
58 pub fn space(&self) -> &ObjectId {
59 &self.space
60 }
61
62 #[must_use]
64 pub fn subjects(&self) -> &[ObjectId] {
65 &self.subjects
66 }
67}
68
69#[derive(Clone, Debug, PartialEq)]
71pub struct WallContact {
72 subject: ObjectId,
73 gap: PlanLength,
74 facing: PlanLength,
75}
76
77impl WallContact {
78 #[must_use]
81 pub fn new(subject: ObjectId, gap: PlanLength, facing: PlanLength) -> Self {
82 Self {
83 subject,
84 gap,
85 facing,
86 }
87 }
88
89 #[must_use]
91 pub fn subject(&self) -> &ObjectId {
92 &self.subject
93 }
94
95 #[must_use]
98 pub fn gap(&self) -> &PlanLength {
99 &self.gap
100 }
101
102 #[must_use]
107 pub fn facing(&self) -> &PlanLength {
108 &self.facing
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
114pub enum EndWall {
115 Decided {
119 start: [f64; 2],
121 end: [f64; 2],
123 contacts: Vec<WallContact>,
125 },
126 Undecided(String),
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct CorridorEnd {
134 point: [f64; 2],
135 clearance: (f64, f64),
136 wall: EndWall,
137}
138
139impl CorridorEnd {
140 pub fn try_new(
143 point: [f64; 2],
144 clearance: (f64, f64),
145 wall: EndWall,
146 ) -> Result<Self, PlanSpanError> {
147 let (lower, upper) = clearance;
148 if !point.iter().all(|value| value.is_finite())
149 || !lower.is_finite()
150 || !upper.is_finite()
151 || lower < 0.0
152 || lower > upper
153 {
154 return Err(PlanSpanError::InvalidMeasurement);
155 }
156 if let EndWall::Decided { start, end, .. } = &wall {
157 #[allow(clippy::float_cmp)]
158 let degenerate = start == end;
159 if degenerate || !start.iter().chain(end).all(|value| value.is_finite()) {
160 return Err(PlanSpanError::InvalidMeasurement);
161 }
162 }
163 Ok(Self {
164 point,
165 clearance,
166 wall,
167 })
168 }
169
170 #[must_use]
173 pub fn point(&self) -> [f64; 2] {
174 self.point
175 }
176
177 #[must_use]
180 pub fn clearance_metres(&self) -> (f64, f64) {
181 self.clearance
182 }
183
184 #[must_use]
186 pub fn wall(&self) -> &EndWall {
187 &self.wall
188 }
189}
190
191#[derive(Clone, Debug, PartialEq)]
193pub struct CorridorEnds {
194 space: ObjectId,
195 ends: Vec<CorridorEnd>,
196 evidence: Evidence,
197}
198
199impl CorridorEnds {
200 pub fn try_new(
203 space: ObjectId,
204 ends: Vec<CorridorEnd>,
205 evidence: Evidence,
206 ) -> Result<Self, PlanSpanError> {
207 if evidence.exact || evidence.locator.trim().is_empty() {
208 return Err(PlanSpanError::InexactEvidence);
209 }
210 Ok(Self {
211 space,
212 ends,
213 evidence,
214 })
215 }
216
217 #[must_use]
219 pub fn space(&self) -> &ObjectId {
220 &self.space
221 }
222
223 #[must_use]
225 pub fn ends(&self) -> &[CorridorEnd] {
226 &self.ends
227 }
228
229 #[must_use]
231 pub fn evidence(&self) -> &Evidence {
232 &self.evidence
233 }
234}
235
236pub(crate) fn bound(
239 request: &CorridorEndRequest,
240 ends: CorridorEnds,
241) -> Result<CorridorEnds, PlanSpanError> {
242 if ends.space() != request.space() {
243 return Err(PlanSpanError::Unavailable(format!(
244 "corridor ends of {} were returned for {}",
245 ends.space(),
246 request.space()
247 )));
248 }
249 for end in ends.ends() {
250 if let EndWall::Decided { contacts, .. } = end.wall() {
251 let named = contacts.iter().map(WallContact::subject);
252 if !named.eq(request.subjects()) {
253 return Err(PlanSpanError::Unavailable(format!(
254 "an end wall of {} was not measured against exactly the requested subjects",
255 request.space()
256 )));
257 }
258 }
259 }
260 Ok(ends)
261}
262
263pub(crate) fn measure(
265 service: &Arc<dyn PlanSpanService>,
266 request: &CorridorEndRequest,
267) -> Result<CorridorEnds, PlanSpanError> {
268 bound(request, service.measure_corridor_ends(request)?)
269}
270
271#[cfg(test)]
272mod tests {
273 use axioval_ir::{Evidence, ObjectId, SourceId};
274
275 use super::{CorridorEnd, CorridorEndRequest, CorridorEnds, EndWall, WallContact, bound};
276 use crate::plan_span::{PlanLength, PlanSpanError};
277
278 fn source() -> SourceId {
279 SourceId::new("cad", "m").unwrap()
280 }
281
282 fn id(local: &str) -> ObjectId {
283 ObjectId::new(source(), local).unwrap()
284 }
285
286 fn approximate() -> Evidence {
287 Evidence {
288 source: source(),
289 locator: "corridor-ends:hall".into(),
290 exact: false,
291 }
292 }
293
294 fn zero() -> PlanLength {
295 PlanLength::try_new(0.0, 0.0, Evidence::exact(source(), "gap")).unwrap()
296 }
297
298 fn end(contacts: Vec<WallContact>) -> CorridorEnd {
299 CorridorEnd::try_new(
300 [0.5, 0.5],
301 (0.5, 0.5),
302 EndWall::Decided {
303 start: [0.0, 1.0],
304 end: [0.0, 0.0],
305 contacts,
306 },
307 )
308 .unwrap()
309 }
310
311 #[test]
312 fn a_request_sorts_its_subjects_and_refuses_the_space() {
313 let request =
314 CorridorEndRequest::try_new(id("hall"), [id("w2"), id("w1"), id("w2")]).unwrap();
315 assert_eq!(request.subjects(), [id("w1"), id("w2")]);
316 assert!(CorridorEndRequest::try_new(id("hall"), [id("hall")]).is_err());
317 }
318
319 #[test]
320 fn corridor_ends_are_never_exact() {
321 let exact = Evidence::exact(source(), "corridor-ends:hall");
322 assert_eq!(
323 CorridorEnds::try_new(id("hall"), Vec::new(), exact),
324 Err(PlanSpanError::InexactEvidence)
325 );
326 assert!(CorridorEnds::try_new(id("hall"), Vec::new(), approximate()).is_ok());
327 }
328
329 #[test]
330 fn an_end_refuses_bad_clearances_and_degenerate_walls() {
331 let wall = || EndWall::Undecided("unused".into());
332 assert!(CorridorEnd::try_new([0.0, 0.0], (0.5, 0.4), wall()).is_err());
333 assert!(CorridorEnd::try_new([0.0, 0.0], (-0.1, 0.4), wall()).is_err());
334 assert!(CorridorEnd::try_new([f64::NAN, 0.0], (0.1, 0.4), wall()).is_err());
335 assert!(
336 CorridorEnd::try_new(
337 [0.0, 0.0],
338 (0.1, 0.1),
339 EndWall::Decided {
340 start: [1.0, 1.0],
341 end: [1.0, 1.0],
342 contacts: Vec::new()
343 }
344 )
345 .is_err()
346 );
347 }
348
349 #[test]
350 fn an_answer_is_bound_to_its_space_and_subjects() {
351 let request = CorridorEndRequest::try_new(id("hall"), [id("w1")]).unwrap();
352 let answer = |space: &str, contacts| {
353 CorridorEnds::try_new(id(space), vec![end(contacts)], approximate()).unwrap()
354 };
355 let contact = |subject: &str| WallContact::new(id(subject), zero(), zero());
356 assert!(bound(&request, answer("hall", vec![contact("w1")])).is_ok());
357 assert!(bound(&request, answer("room", vec![contact("w1")])).is_err());
358 assert!(bound(&request, answer("hall", vec![contact("w2")])).is_err());
359 assert!(bound(&request, answer("hall", Vec::new())).is_err());
360 assert!(bound(&request, answer("hall", vec![contact("w1"), contact("w1")])).is_err());
361 }
362}