1use axioval_ir::{Evidence, ObjectId};
28
29use crate::LengthInterval;
30use crate::door_leaves::{SweptDoor, tidy_swept};
31use crate::free_space::{ElevationBand, FreeSpaceError};
32use crate::services::reviewable_exact_evidence;
33
34#[derive(Clone, Debug, PartialEq)]
40pub struct CirculationRequest {
41 scope: ObjectId,
42 entrances: Vec<ObjectId>,
43 components: Vec<ObjectId>,
44 obstacles: Vec<ObjectId>,
45 width_metres: f64,
46 height_metres: f64,
47 tolerance_metres: f64,
48 swept: Vec<SweptDoor>,
49 merged: Vec<ObjectId>,
50 band_from_metres: f64,
51}
52
53impl CirculationRequest {
54 pub fn try_new(
64 scope: ObjectId,
65 entrances: Vec<ObjectId>,
66 components: Vec<ObjectId>,
67 obstacles: Vec<ObjectId>,
68 width_metres: f64,
69 height_metres: f64,
70 tolerance_metres: f64,
71 ) -> Result<Self, FreeSpaceError> {
72 let positive = |value: f64| value.is_finite() && value > 0.0;
73 if !(positive(width_metres)
74 && positive(height_metres)
75 && tolerance_metres.is_finite()
76 && tolerance_metres >= 0.0)
77 {
78 return Err(FreeSpaceError::InvalidClearanceShape);
79 }
80 let tidy = |mut list: Vec<ObjectId>| {
81 list.retain(|object| object != &scope);
82 list.sort();
83 list.dedup();
84 list
85 };
86 let entrances = tidy(entrances);
87 let components = tidy(components);
88 let mut obstacles = tidy(obstacles);
89 obstacles.retain(|object| entrances.binary_search(object).is_err());
90 Ok(Self {
91 scope,
92 entrances,
93 components,
94 obstacles,
95 width_metres,
96 height_metres,
97 tolerance_metres,
98 swept: Vec::new(),
99 merged: Vec::new(),
100 band_from_metres: 0.0,
101 })
102 }
103
104 pub fn with_merged_scopes(mut self, mut merged: Vec<ObjectId>) -> Result<Self, FreeSpaceError> {
113 merged.retain(|object| object != &self.scope);
114 merged.sort();
115 merged.dedup();
116 if merged.iter().any(|object| {
117 self.entrances.binary_search(object).is_ok()
118 || self.components.binary_search(object).is_ok()
119 }) {
120 return Err(FreeSpaceError::MergedScopeConflict);
121 }
122 self.obstacles
123 .retain(|object| merged.binary_search(object).is_err());
124 self.merged = merged;
125 Ok(self)
126 }
127
128 #[must_use]
130 pub fn merged_scopes(&self) -> &[ObjectId] {
131 &self.merged
132 }
133
134 pub fn with_band_from(mut self, from_metres: f64) -> Result<Self, FreeSpaceError> {
142 ElevationBand::try_new(from_metres, self.height_metres)?;
143 self.band_from_metres = from_metres;
144 Ok(self)
145 }
146
147 pub fn with_swept_doors(mut self, swept: Vec<SweptDoor>) -> Result<Self, FreeSpaceError> {
158 let mut swept = tidy_swept(swept).ok_or(FreeSpaceError::ConflictingSweptDoors)?;
159 swept.retain(|door| self.entrances.binary_search(door.door()).is_err());
160 self.swept = swept;
161 Ok(self)
162 }
163
164 #[must_use]
166 pub fn swept_doors(&self) -> &[SweptDoor] {
167 &self.swept
168 }
169
170 #[must_use]
172 pub fn scope(&self) -> &ObjectId {
173 &self.scope
174 }
175
176 #[must_use]
178 pub fn entrances(&self) -> &[ObjectId] {
179 &self.entrances
180 }
181
182 #[must_use]
184 pub fn components(&self) -> &[ObjectId] {
185 &self.components
186 }
187
188 #[must_use]
191 pub fn obstacles(&self) -> &[ObjectId] {
192 &self.obstacles
193 }
194
195 #[must_use]
197 pub fn width_metres(&self) -> f64 {
198 self.width_metres
199 }
200
201 #[must_use]
203 pub fn height_metres(&self) -> f64 {
204 self.height_metres
205 }
206
207 #[must_use]
210 pub fn tolerance_metres(&self) -> f64 {
211 self.tolerance_metres
212 }
213
214 #[must_use]
217 pub fn band(&self) -> ElevationBand {
218 ElevationBand::try_new(self.band_from_metres, self.height_metres)
219 .unwrap_or_else(|_| unreachable!("the start lies below the positive, finite height"))
220 }
221
222 #[must_use]
225 pub fn subjects(&self) -> Vec<ObjectId> {
226 let mut subjects: Vec<ObjectId> = self
227 .entrances
228 .iter()
229 .chain(&self.components)
230 .cloned()
231 .collect();
232 subjects.sort();
233 subjects.dedup();
234 subjects
235 }
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240#[non_exhaustive]
241pub enum CirculationNodeKind {
242 End,
244 Path,
246 Junction,
248 Isolated,
250}
251
252impl CirculationNodeKind {
253 fn admits(self, degree: usize) -> bool {
254 match self {
255 Self::End => degree == 1,
256 Self::Path => degree == 2,
257 Self::Junction => degree >= 3,
258 Self::Isolated => degree == 0,
259 }
260 }
261}
262
263#[derive(Clone, Debug, PartialEq)]
265pub struct CirculationNode {
266 point: [f64; 3],
267 kind: CirculationNodeKind,
268 piece: usize,
269 half_width: LengthInterval,
270}
271
272impl CirculationNode {
273 pub fn try_new(
280 point: [f64; 3],
281 kind: CirculationNodeKind,
282 piece: usize,
283 half_width: LengthInterval,
284 ) -> Result<Self, FreeSpaceError> {
285 if !point.iter().all(|value| value.is_finite()) {
286 return Err(FreeSpaceError::Unavailable(
287 "a circulation node's coordinates must be finite".into(),
288 ));
289 }
290 Ok(Self {
291 point,
292 kind,
293 piece,
294 half_width,
295 })
296 }
297
298 #[must_use]
301 pub fn point(&self) -> [f64; 3] {
302 self.point
303 }
304
305 #[must_use]
307 pub fn kind(&self) -> CirculationNodeKind {
308 self.kind
309 }
310
311 #[must_use]
313 pub fn piece(&self) -> usize {
314 self.piece
315 }
316
317 #[must_use]
320 pub fn half_width(&self) -> LengthInterval {
321 self.half_width
322 }
323}
324
325#[derive(Clone, Debug, PartialEq, Eq)]
327pub struct CirculationContact {
328 subject: ObjectId,
329 reached: Vec<(usize, Option<usize>)>,
330 possible: Vec<usize>,
331}
332
333impl CirculationContact {
334 #[must_use]
338 pub fn new(
339 subject: ObjectId,
340 mut reached: Vec<(usize, Option<usize>)>,
341 mut possible: Vec<usize>,
342 ) -> Self {
343 reached.sort_unstable();
344 reached.dedup_by_key(|(piece, _)| *piece);
345 possible.sort_unstable();
346 possible.dedup();
347 Self {
348 subject,
349 reached,
350 possible,
351 }
352 }
353
354 #[must_use]
356 pub fn subject(&self) -> &ObjectId {
357 &self.subject
358 }
359
360 #[must_use]
362 pub fn reached(&self) -> &[(usize, Option<usize>)] {
363 &self.reached
364 }
365
366 #[must_use]
368 pub fn reaches(&self, piece: usize) -> bool {
369 self.reached.iter().any(|(reached, _)| *reached == piece)
370 }
371
372 #[must_use]
375 pub fn possible(&self) -> &[usize] {
376 &self.possible
377 }
378}
379
380#[derive(Clone, Debug, PartialEq)]
382pub struct CirculationMap {
383 request: CirculationRequest,
384 pieces: usize,
385 possible_pieces: usize,
386 nodes: Vec<CirculationNode>,
387 edges: Vec<(usize, usize)>,
388 spacing_metres: f64,
389 contacts: Vec<CirculationContact>,
390 unmapped: Vec<(usize, String)>,
391 evidence: Evidence,
392}
393
394impl CirculationMap {
395 #[allow(clippy::too_many_arguments)]
407 pub fn try_new(
408 request: CirculationRequest,
409 pieces: usize,
410 possible_pieces: usize,
411 nodes: Vec<CirculationNode>,
412 mut edges: Vec<(usize, usize)>,
413 spacing_metres: f64,
414 contacts: Vec<CirculationContact>,
415 mut unmapped: Vec<(usize, String)>,
416 evidence: Evidence,
417 ) -> Result<Self, FreeSpaceError> {
418 let malformed = |why: &str| FreeSpaceError::Unavailable(format!("circulation map: {why}"));
419 if !reviewable_exact_evidence(&evidence) {
420 return Err(FreeSpaceError::InexactPlacementEvidence);
421 }
422 if !(spacing_metres.is_finite() && spacing_metres > 0.0) {
423 return Err(malformed("the sample spacing must be positive"));
424 }
425 if nodes.iter().any(|node| node.piece >= pieces) {
426 return Err(malformed("a node lies in no piece"));
427 }
428 unmapped.sort();
429 unmapped.dedup_by_key(|(piece, _)| *piece);
430 if unmapped
431 .iter()
432 .any(|(piece, _)| *piece >= pieces || nodes.iter().any(|node| node.piece == *piece))
433 {
434 return Err(malformed("an unmapped piece is not there or has nodes"));
435 }
436 edges.sort_unstable();
437 edges.dedup();
438 let mut degree = vec![0usize; nodes.len()];
439 for &(a, b) in &edges {
440 if a >= b || b >= nodes.len() || nodes[a].piece != nodes[b].piece {
441 return Err(malformed("an edge joins no two nodes of one piece"));
442 }
443 degree[a] += 1;
444 degree[b] += 1;
445 }
446 if nodes
447 .iter()
448 .zip(°ree)
449 .any(|(node, °ree)| !node.kind.admits(degree))
450 {
451 return Err(malformed("a node's kind does not match its neighbours"));
452 }
453 let subjects = request.subjects();
454 if contacts.len() != subjects.len()
455 || contacts
456 .iter()
457 .zip(&subjects)
458 .any(|(contact, subject)| &contact.subject != subject)
459 {
460 return Err(malformed("the contacts are not the request's subjects"));
461 }
462 for contact in &contacts {
463 for &(piece, node) in &contact.reached {
464 if piece >= pieces
465 || node.is_some_and(|node| node >= nodes.len() || nodes[node].piece != piece)
466 {
467 return Err(malformed(
468 "a contact names a piece or node that is not there",
469 ));
470 }
471 }
472 if contact
473 .possible
474 .iter()
475 .any(|&piece| piece >= possible_pieces)
476 {
477 return Err(malformed(
478 "a contact names a possible piece that is not there",
479 ));
480 }
481 }
482 Ok(Self {
483 request,
484 pieces,
485 possible_pieces,
486 nodes,
487 edges,
488 spacing_metres,
489 contacts,
490 unmapped,
491 evidence,
492 })
493 }
494
495 #[must_use]
497 pub fn request(&self) -> &CirculationRequest {
498 &self.request
499 }
500
501 #[must_use]
504 pub fn pieces(&self) -> usize {
505 self.pieces
506 }
507
508 #[must_use]
511 pub fn possible_pieces(&self) -> usize {
512 self.possible_pieces
513 }
514
515 #[must_use]
517 pub fn nodes(&self) -> &[CirculationNode] {
518 &self.nodes
519 }
520
521 #[must_use]
523 pub fn edges(&self) -> &[(usize, usize)] {
524 &self.edges
525 }
526
527 #[must_use]
529 pub fn neighbours(&self, node: usize) -> Vec<usize> {
530 let mut out: Vec<usize> = self
531 .edges
532 .iter()
533 .filter_map(|&(a, b)| match (a == node, b == node) {
534 (true, _) => Some(b),
535 (_, true) => Some(a),
536 _ => None,
537 })
538 .collect();
539 out.sort_unstable();
540 out
541 }
542
543 #[must_use]
546 pub fn spacing_metres(&self) -> f64 {
547 self.spacing_metres
548 }
549
550 #[must_use]
552 pub fn contacts(&self) -> &[CirculationContact] {
553 &self.contacts
554 }
555
556 #[must_use]
558 pub fn contact(&self, subject: &ObjectId) -> Option<&CirculationContact> {
559 self.contacts
560 .iter()
561 .find(|contact| &contact.subject == subject)
562 }
563
564 #[must_use]
567 pub fn unmapped(&self) -> &[(usize, String)] {
568 &self.unmapped
569 }
570
571 #[must_use]
573 pub fn unmapped_reason(&self, piece: usize) -> Option<&str> {
574 self.unmapped
575 .iter()
576 .find(|(unmapped, _)| *unmapped == piece)
577 .map(|(_, why)| why.as_str())
578 }
579
580 #[must_use]
582 pub fn evidence(&self) -> &Evidence {
583 &self.evidence
584 }
585}