1use std::sync::Arc;
53
54use axioval_ir::{Evidence, ObjectId};
55use thiserror::Error;
56
57use crate::{ConvexPlanRegion, ElevationInterval, MetricDirection};
58
59#[derive(Clone, Debug, Error, PartialEq, Eq)]
61pub enum WalkingSurfaceError {
62 #[error("no geometry for `{0}`")]
64 UnknownObject(ObjectId),
65 #[error("walking surface unavailable: {0}")]
68 Unavailable(String),
69 #[error("walking surface unsupported: {0}")]
72 Unsupported(String),
73 #[error("walking surface is inexact: {0}")]
76 InexactGeometry(String),
77 #[error("walking surface measurement is invalid")]
80 InvalidMeasurement,
81 #[error("walking surface evidence does not match its exactness")]
83 InexactEvidence,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq)]
88pub struct MeasuredInterval {
89 lower: f64,
90 upper: f64,
91}
92
93impl MeasuredInterval {
94 pub fn try_new(lower: f64, upper: f64) -> Result<Self, WalkingSurfaceError> {
96 if !lower.is_finite() || !upper.is_finite() || lower > upper {
97 return Err(WalkingSurfaceError::InvalidMeasurement);
98 }
99 Ok(Self { lower, upper })
100 }
101
102 #[must_use]
104 pub fn lower(&self) -> f64 {
105 self.lower
106 }
107
108 #[must_use]
110 pub fn upper(&self) -> f64 {
111 self.upper
112 }
113
114 #[must_use]
116 #[allow(clippy::float_cmp)]
117 pub fn is_point(&self) -> bool {
118 self.lower == self.upper
119 }
120}
121
122fn between(a: ElevationInterval, b: ElevationInterval) -> MeasuredInterval {
125 let low = subtract_down(a.lower_metres(), b.upper_metres());
126 let high = subtract_up(a.upper_metres(), b.lower_metres());
127 MeasuredInterval {
128 lower: low,
129 upper: high.max(low),
130 }
131}
132
133fn subtract_down(x: f64, y: f64) -> f64 {
135 let (rounded, error) = two_difference(x, y);
136 if error < 0.0 {
137 rounded.next_down()
138 } else {
139 rounded
140 }
141}
142
143fn subtract_up(x: f64, y: f64) -> f64 {
145 let (rounded, error) = two_difference(x, y);
146 if error > 0.0 {
147 rounded.next_up()
148 } else {
149 rounded
150 }
151}
152
153fn two_difference(x: f64, y: f64) -> (f64, f64) {
156 let rounded = x - y;
157 let back = rounded - x;
158 let error = (x - (rounded - back)) + (-y - back);
159 (rounded, error)
160}
161
162fn divide(x: f64, y: f64, up: bool) -> f64 {
165 let rounded = x / y;
166 let residual = rounded.mul_add(y, -x);
169 match (up, residual) {
170 (true, residual) if residual < 0.0 => rounded.next_up(),
171 (false, residual) if residual > 0.0 => rounded.next_down(),
172 _ => rounded,
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq)]
182pub struct PlanSegment {
183 from: [f64; 2],
184 to: [f64; 2],
185 radius: f64,
186}
187
188impl PlanSegment {
189 pub fn try_new(from: [f64; 2], to: [f64; 2], radius: f64) -> Result<Self, WalkingSurfaceError> {
193 let finite = from.iter().chain(&to).all(|value| value.is_finite());
194 #[allow(clippy::float_cmp)]
195 if !finite || from == to || !radius.is_finite() || radius < 0.0 {
196 return Err(WalkingSurfaceError::InvalidMeasurement);
197 }
198 Ok(Self { from, to, radius })
199 }
200
201 #[must_use]
203 pub fn from(&self) -> [f64; 2] {
204 self.from
205 }
206
207 #[must_use]
209 pub fn to(&self) -> [f64; 2] {
210 self.to
211 }
212
213 #[must_use]
215 pub fn radius(&self) -> f64 {
216 self.radius
217 }
218
219 #[must_use]
224 pub fn angle_to(&self, other: &PlanSegment) -> Option<MeasuredInterval> {
225 let turn = |segment: &PlanSegment| -> Option<([f64; 2], f64)> {
229 let vector = [
230 segment.to[0] - segment.from[0],
231 segment.to[1] - segment.from[1],
232 ];
233 let magnitude = segment
234 .from
235 .iter()
236 .chain(&segment.to)
237 .fold(0.0_f64, |most, value| most.max(value.abs()));
238 let radius = 4.0f64.mul_add(f64::EPSILON * magnitude, segment.radius);
239 let length = vector[0].hypot(vector[1]) * 4.0f64.mul_add(-f64::EPSILON, 1.0);
240 if length <= 2.0 * radius {
241 return None;
242 }
243 Some((vector, (2.0 * radius / length).asin()))
244 };
245 let (u, first) = turn(self)?;
246 let (v, second) = turn(other)?;
247 let cross = u[0].mul_add(v[1], -(u[1] * v[0]));
248 let dot = u[0].mul_add(v[0], u[1] * v[1]);
249 let angle = cross.abs().atan2(dot.abs());
250 let spread = first + second + 16.0 * f64::EPSILON;
253 let lower = (angle - spread).max(0.0);
254 let upper = (angle + spread).min(std::f64::consts::FRAC_PI_2).max(lower);
255 MeasuredInterval::try_new(lower, upper).ok()
256 }
257
258 fn is_exact(&self) -> bool {
259 self.radius == 0.0
260 }
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub enum RiserClosure {
266 Closed,
268 Open,
271 NotMeasured,
274}
275
276#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct Tread {
284 elevation: ElevationInterval,
285 front: ElevationInterval,
286 back: ElevationInterval,
287 sides: Option<Sides>,
288 nosing: Option<PlanSegment>,
289 riser_below: RiserClosure,
290}
291
292type Sides = (ElevationInterval, ElevationInterval);
295
296#[must_use]
299pub fn across(direction: MetricDirection) -> MetricDirection {
300 let [x, y, _] = direction.components();
301 MetricDirection::try_new([-y, x, 0.0]).unwrap_or(direction)
304}
305
306fn sides(left: ElevationInterval, right: ElevationInterval) -> Result<Sides, WalkingSurfaceError> {
307 if left.lower_metres() > right.lower_metres() || left.upper_metres() > right.upper_metres() {
308 return Err(WalkingSurfaceError::InvalidMeasurement);
309 }
310 Ok((left, right))
311}
312
313fn sides_exact(sides: Option<Sides>) -> bool {
314 sides.is_none_or(|(left, right)| left.is_exact() && right.is_exact())
315}
316
317fn least(widths: impl Iterator<Item = MeasuredInterval>) -> Option<MeasuredInterval> {
319 widths.reduce(|least, width| MeasuredInterval {
320 lower: least.lower.min(width.lower),
321 upper: least.upper.min(width.upper),
322 })
323}
324
325impl Tread {
326 pub fn try_new(
329 elevation: ElevationInterval,
330 front: ElevationInterval,
331 back: ElevationInterval,
332 ) -> Result<Self, WalkingSurfaceError> {
333 if front.lower_metres() > back.lower_metres() || front.upper_metres() > back.upper_metres()
334 {
335 return Err(WalkingSurfaceError::InvalidMeasurement);
336 }
337 Ok(Self {
338 elevation,
339 front,
340 back,
341 sides: None,
342 nosing: None,
343 riser_below: RiserClosure::NotMeasured,
344 })
345 }
346
347 pub fn with_sides(
354 mut self,
355 left: ElevationInterval,
356 right: ElevationInterval,
357 ) -> Result<Self, WalkingSurfaceError> {
358 self.sides = Some(sides(left, right)?);
359 Ok(self)
360 }
361
362 #[must_use]
364 pub fn with_nosing(mut self, nosing: PlanSegment) -> Self {
365 self.nosing = Some(nosing);
366 self
367 }
368
369 #[must_use]
371 pub fn with_riser_below(mut self, riser: RiserClosure) -> Self {
372 self.riser_below = riser;
373 self
374 }
375
376 #[must_use]
379 pub fn sides(&self) -> Option<(ElevationInterval, ElevationInterval)> {
380 self.sides
381 }
382
383 #[must_use]
386 pub fn width(&self) -> Option<MeasuredInterval> {
387 self.sides.map(|(left, right)| between(right, left))
388 }
389
390 #[must_use]
392 pub fn elevation(&self) -> ElevationInterval {
393 self.elevation
394 }
395
396 #[must_use]
398 pub fn front(&self) -> ElevationInterval {
399 self.front
400 }
401
402 #[must_use]
404 pub fn back(&self) -> ElevationInterval {
405 self.back
406 }
407
408 #[must_use]
410 pub fn nosing(&self) -> Option<PlanSegment> {
411 self.nosing
412 }
413
414 #[must_use]
416 pub fn riser_below(&self) -> RiserClosure {
417 self.riser_below
418 }
419
420 #[must_use]
422 pub fn depth(&self) -> MeasuredInterval {
423 between(self.back, self.front)
424 }
425
426 #[must_use]
428 pub fn is_exact(&self) -> bool {
429 self.elevation.is_exact()
430 && self.front.is_exact()
431 && self.back.is_exact()
432 && sides_exact(self.sides)
433 && self.nosing.is_none_or(|nosing| nosing.is_exact())
434 }
435}
436
437#[derive(Clone, Copy, Debug, PartialEq)]
439pub enum WalkingLinePlacement {
440 Centre,
442 FromInnerSide(f64),
445}
446
447#[derive(Clone, Debug, PartialEq)]
451pub struct TreadFlightRequest {
452 object: ObjectId,
453 walking_line: WalkingLinePlacement,
454}
455
456impl TreadFlightRequest {
457 #[must_use]
459 pub fn new(object: ObjectId) -> Self {
460 Self {
461 object,
462 walking_line: WalkingLinePlacement::Centre,
463 }
464 }
465
466 pub fn from_inner_side(object: ObjectId, offset: f64) -> Result<Self, WalkingSurfaceError> {
469 if !offset.is_finite() || offset <= 0.0 {
470 return Err(WalkingSurfaceError::InvalidMeasurement);
471 }
472 Ok(Self {
473 object,
474 walking_line: WalkingLinePlacement::FromInnerSide(offset),
475 })
476 }
477
478 #[must_use]
480 pub fn object(&self) -> &ObjectId {
481 &self.object
482 }
483
484 #[must_use]
486 pub fn walking_line(&self) -> WalkingLinePlacement {
487 self.walking_line
488 }
489}
490
491#[derive(Clone, Debug, PartialEq)]
493pub enum WalkingLine {
494 Straight(MetricDirection),
497 Turning(Vec<[f64; 2]>),
502}
503
504impl WalkingLine {
505 #[must_use]
507 pub fn is_turning(&self) -> bool {
508 matches!(self, Self::Turning(_))
509 }
510
511 #[allow(clippy::float_cmp)]
512 fn is_valid(&self) -> bool {
513 match self {
514 Self::Straight(direction) => direction.components()[2] == 0.0,
515 Self::Turning(vertices) => {
516 vertices.len() >= 2
517 && vertices.iter().flatten().all(|value| value.is_finite())
518 && vertices.windows(2).all(|pair| pair[0] != pair[1])
519 }
520 }
521 }
522}
523
524#[derive(Clone, Debug, PartialEq)]
535pub struct TreadFlight {
536 request: TreadFlightRequest,
537 walking_line: WalkingLine,
538 base: ElevationInterval,
539 top: ElevationInterval,
540 treads: Vec<Tread>,
541 ends_in_riser: bool,
542 final_riser: RiserClosure,
543 evidence: Evidence,
544}
545
546impl TreadFlight {
547 pub fn try_new(
555 request: TreadFlightRequest,
556 walking_line: WalkingLine,
557 base: ElevationInterval,
558 top: ElevationInterval,
559 treads: Vec<Tread>,
560 evidence: Evidence,
561 ) -> Result<Self, WalkingSurfaceError> {
562 if !walking_line.is_valid() {
563 return Err(WalkingSurfaceError::InvalidMeasurement);
564 }
565 let (Some(first), Some(last)) = (treads.first(), treads.last()) else {
566 return Err(WalkingSurfaceError::InvalidMeasurement);
567 };
568 if base.upper_metres() >= first.elevation.lower_metres() {
569 return Err(WalkingSurfaceError::InvalidMeasurement);
570 }
571 for pair in treads.windows(2) {
572 if pair[0].elevation.upper_metres() >= pair[1].elevation.lower_metres()
573 || pair[0].front.upper_metres() >= pair[1].front.lower_metres()
574 {
575 return Err(WalkingSurfaceError::InvalidMeasurement);
576 }
577 }
578 let ends_in_riser = if top.lower_metres() > last.elevation.upper_metres() {
579 true
580 } else if top.upper_metres() <= last.elevation.upper_metres()
581 && top.upper_metres() >= last.elevation.lower_metres()
582 {
583 false
585 } else {
586 return Err(WalkingSurfaceError::InvalidMeasurement);
588 };
589 let exact = base.is_exact() && top.is_exact() && treads.iter().all(Tread::is_exact);
590 if evidence.exact != exact || evidence.locator.trim().is_empty() {
591 return Err(WalkingSurfaceError::InexactEvidence);
592 }
593 Ok(Self {
594 request,
595 walking_line,
596 base,
597 top,
598 treads,
599 ends_in_riser,
600 final_riser: RiserClosure::NotMeasured,
601 evidence,
602 })
603 }
604
605 #[must_use]
609 pub fn with_final_riser(mut self, riser: RiserClosure) -> Self {
610 self.final_riser = riser;
611 self
612 }
613
614 #[must_use]
616 pub fn request(&self) -> &TreadFlightRequest {
617 &self.request
618 }
619
620 #[must_use]
622 pub fn object(&self) -> &ObjectId {
623 &self.request.object
624 }
625
626 #[must_use]
628 pub fn walking_line(&self) -> &WalkingLine {
629 &self.walking_line
630 }
631
632 #[must_use]
634 pub fn base(&self) -> ElevationInterval {
635 self.base
636 }
637
638 #[must_use]
640 pub fn top(&self) -> ElevationInterval {
641 self.top
642 }
643
644 #[must_use]
646 pub fn treads(&self) -> &[Tread] {
647 &self.treads
648 }
649
650 #[must_use]
652 pub fn ends_in_riser(&self) -> bool {
653 self.ends_in_riser
654 }
655
656 #[must_use]
661 pub fn risers(&self) -> Vec<MeasuredInterval> {
662 let mut levels = vec![self.base];
663 levels.extend(self.treads.iter().map(|tread| tread.elevation));
664 if self.ends_in_riser {
665 levels.push(self.top);
666 }
667 levels
668 .windows(2)
669 .map(|pair| between(pair[1], pair[0]))
670 .collect()
671 }
672
673 #[must_use]
677 pub fn riser_closures(&self) -> Vec<RiserClosure> {
678 let mut closures: Vec<RiserClosure> =
679 self.treads.iter().map(|tread| tread.riser_below).collect();
680 if self.ends_in_riser {
681 closures.push(self.final_riser);
682 }
683 closures
684 }
685
686 #[must_use]
690 pub fn goings(&self) -> Vec<MeasuredInterval> {
691 self.treads
692 .windows(2)
693 .map(|pair| between(pair[1].front, pair[0].front))
694 .collect()
695 }
696
697 #[must_use]
701 pub fn nosings(&self) -> Vec<MeasuredInterval> {
702 self.treads
703 .windows(2)
704 .map(|pair| between(pair[0].back, pair[1].front))
705 .collect()
706 }
707
708 #[must_use]
713 pub fn winder_angles(&self) -> Vec<Option<MeasuredInterval>> {
714 self.treads
715 .windows(2)
716 .map(|pair| match (pair[0].nosing, pair[1].nosing) {
717 (Some(lower), Some(upper)) => lower.angle_to(&upper),
718 _ => None,
719 })
720 .collect()
721 }
722
723 #[must_use]
725 pub fn rise(&self) -> MeasuredInterval {
726 let summit = if self.ends_in_riser {
727 self.top
728 } else {
729 self.treads.last().map_or(self.top, |tread| tread.elevation)
730 };
731 between(summit, self.base)
732 }
733
734 #[must_use]
737 pub fn width(&self) -> Option<MeasuredInterval> {
738 let widths: Option<Vec<MeasuredInterval>> = self.treads.iter().map(Tread::width).collect();
739 least(widths?.into_iter())
740 }
741
742 #[must_use]
744 pub fn is_exact(&self) -> bool {
745 self.evidence.exact
746 }
747
748 #[must_use]
750 pub fn evidence(&self) -> &Evidence {
751 &self.evidence
752 }
753}
754
755#[derive(Clone, Copy, Debug, PartialEq)]
761pub struct SlopedRun {
762 direction: MetricDirection,
763 bottom: ElevationInterval,
764 top: ElevationInterval,
765 start: ElevationInterval,
766 end: ElevationInterval,
767 sides: Option<Sides>,
768}
769
770impl SlopedRun {
771 pub fn try_new(
775 direction: MetricDirection,
776 bottom: ElevationInterval,
777 top: ElevationInterval,
778 start: ElevationInterval,
779 end: ElevationInterval,
780 ) -> Result<Self, WalkingSurfaceError> {
781 #[allow(clippy::float_cmp)]
782 if direction.components()[2] != 0.0
783 || bottom.upper_metres() >= top.lower_metres()
784 || start.upper_metres() >= end.lower_metres()
785 {
786 return Err(WalkingSurfaceError::InvalidMeasurement);
787 }
788 Ok(Self {
789 direction,
790 bottom,
791 top,
792 start,
793 end,
794 sides: None,
795 })
796 }
797
798 pub fn with_sides(
802 mut self,
803 left: ElevationInterval,
804 right: ElevationInterval,
805 ) -> Result<Self, WalkingSurfaceError> {
806 self.sides = Some(sides(left, right)?);
807 Ok(self)
808 }
809
810 #[must_use]
812 pub fn sides(&self) -> Option<(ElevationInterval, ElevationInterval)> {
813 self.sides
814 }
815
816 #[must_use]
818 pub fn width(&self) -> Option<MeasuredInterval> {
819 self.sides.map(|(left, right)| between(right, left))
820 }
821
822 #[must_use]
824 pub fn direction(&self) -> MetricDirection {
825 self.direction
826 }
827
828 #[must_use]
830 pub fn bottom(&self) -> ElevationInterval {
831 self.bottom
832 }
833
834 #[must_use]
836 pub fn top(&self) -> ElevationInterval {
837 self.top
838 }
839
840 #[must_use]
842 pub fn start(&self) -> ElevationInterval {
843 self.start
844 }
845
846 #[must_use]
848 pub fn end(&self) -> ElevationInterval {
849 self.end
850 }
851
852 #[must_use]
854 pub fn rise(&self) -> MeasuredInterval {
855 between(self.top, self.bottom)
856 }
857
858 #[must_use]
860 pub fn length(&self) -> MeasuredInterval {
861 between(self.end, self.start)
862 }
863
864 #[must_use]
868 pub fn slope(&self) -> MeasuredInterval {
869 let (rise, length) = (self.rise(), self.length());
870 let lower = divide(rise.lower.max(0.0), length.upper, false);
871 let upper = divide(rise.upper, length.lower, true);
872 MeasuredInterval {
873 lower,
874 upper: upper.max(lower),
875 }
876 }
877
878 #[must_use]
880 pub fn is_exact(&self) -> bool {
881 self.bottom.is_exact()
882 && self.top.is_exact()
883 && self.start.is_exact()
884 && self.end.is_exact()
885 && sides_exact(self.sides)
886 }
887}
888
889#[derive(Clone, Debug, PartialEq)]
892pub struct SlopedSurface {
893 object: ObjectId,
894 runs: Vec<SlopedRun>,
895 evidence: Evidence,
896}
897
898impl SlopedSurface {
899 pub fn try_new(
902 object: ObjectId,
903 runs: Vec<SlopedRun>,
904 evidence: Evidence,
905 ) -> Result<Self, WalkingSurfaceError> {
906 if runs.is_empty() {
907 return Err(WalkingSurfaceError::InvalidMeasurement);
908 }
909 let exact = runs.iter().all(SlopedRun::is_exact);
910 if evidence.exact != exact || evidence.locator.trim().is_empty() {
911 return Err(WalkingSurfaceError::InexactEvidence);
912 }
913 Ok(Self {
914 object,
915 runs,
916 evidence,
917 })
918 }
919
920 #[must_use]
922 pub fn object(&self) -> &ObjectId {
923 &self.object
924 }
925
926 #[must_use]
928 pub fn runs(&self) -> &[SlopedRun] {
929 &self.runs
930 }
931
932 #[must_use]
934 pub fn is_exact(&self) -> bool {
935 self.evidence.exact
936 }
937
938 #[must_use]
940 pub fn evidence(&self) -> &Evidence {
941 &self.evidence
942 }
943}
944
945#[derive(Clone, Debug, PartialEq, Eq)]
950pub struct HeadroomRequest {
951 subject: ObjectId,
952 obstacles: Vec<ObjectId>,
953}
954
955impl HeadroomRequest {
956 #[must_use]
958 pub fn new(subject: ObjectId, obstacles: impl IntoIterator<Item = ObjectId>) -> Self {
959 let mut obstacles: Vec<ObjectId> = obstacles
960 .into_iter()
961 .filter(|obstacle| *obstacle != subject)
962 .collect();
963 obstacles.sort();
964 obstacles.dedup();
965 Self { subject, obstacles }
966 }
967
968 #[must_use]
970 pub fn subject(&self) -> &ObjectId {
971 &self.subject
972 }
973
974 #[must_use]
976 pub fn obstacles(&self) -> &[ObjectId] {
977 &self.obstacles
978 }
979}
980
981#[derive(Clone, Debug, PartialEq)]
988pub struct Headroom {
989 request: HeadroomRequest,
990 clearance: Option<MeasuredInterval>,
991 governing: Vec<ObjectId>,
992 evidence: Evidence,
993}
994
995impl Headroom {
996 pub fn try_new(
1001 request: HeadroomRequest,
1002 clearance: Option<MeasuredInterval>,
1003 governing: Vec<ObjectId>,
1004 evidence: Evidence,
1005 ) -> Result<Self, WalkingSurfaceError> {
1006 let governing = governed(&request.obstacles, clearance, governing, &evidence)?;
1007 Ok(Self {
1008 request,
1009 clearance,
1010 governing,
1011 evidence,
1012 })
1013 }
1014
1015 #[must_use]
1017 pub fn request(&self) -> &HeadroomRequest {
1018 &self.request
1019 }
1020
1021 #[must_use]
1023 pub fn clearance(&self) -> Option<MeasuredInterval> {
1024 self.clearance
1025 }
1026
1027 #[must_use]
1029 pub fn governing(&self) -> &[ObjectId] {
1030 &self.governing
1031 }
1032
1033 #[must_use]
1035 pub fn evidence(&self) -> &Evidence {
1036 &self.evidence
1037 }
1038}
1039
1040#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1049pub enum WalkingEnd {
1050 FlightBottom,
1052 FlightTop,
1054 RunBottom(usize),
1057 RunTop(usize),
1059}
1060
1061#[derive(Clone, Debug, PartialEq, Eq)]
1068pub struct LandingRequest {
1069 subject: ObjectId,
1070 end: WalkingEnd,
1071 candidates: Vec<ObjectId>,
1072}
1073
1074impl LandingRequest {
1075 #[must_use]
1077 pub fn new(
1078 subject: ObjectId,
1079 end: WalkingEnd,
1080 candidates: impl IntoIterator<Item = ObjectId>,
1081 ) -> Self {
1082 let mut candidates: Vec<ObjectId> = candidates
1083 .into_iter()
1084 .filter(|candidate| *candidate != subject)
1085 .collect();
1086 candidates.sort();
1087 candidates.dedup();
1088 Self {
1089 subject,
1090 end,
1091 candidates,
1092 }
1093 }
1094
1095 #[must_use]
1097 pub fn subject(&self) -> &ObjectId {
1098 &self.subject
1099 }
1100
1101 #[must_use]
1103 pub fn end(&self) -> WalkingEnd {
1104 self.end
1105 }
1106
1107 #[must_use]
1109 pub fn candidates(&self) -> &[ObjectId] {
1110 &self.candidates
1111 }
1112}
1113
1114#[derive(Clone, Copy, Debug, PartialEq)]
1119pub struct LandingExtent {
1120 far: ElevationInterval,
1121 left: ElevationInterval,
1122 right: ElevationInterval,
1123}
1124
1125impl LandingExtent {
1126 pub fn try_new(
1129 far: ElevationInterval,
1130 left: ElevationInterval,
1131 right: ElevationInterval,
1132 ) -> Result<Self, WalkingSurfaceError> {
1133 let (left, right) = sides(left, right)?;
1134 Ok(Self { far, left, right })
1135 }
1136
1137 #[must_use]
1139 pub fn far(&self) -> ElevationInterval {
1140 self.far
1141 }
1142
1143 #[must_use]
1145 pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1146 (self.left, self.right)
1147 }
1148
1149 fn is_exact(&self) -> bool {
1150 self.far.is_exact() && sides_exact(Some((self.left, self.right)))
1151 }
1152}
1153
1154#[derive(Clone, Debug, PartialEq)]
1157pub struct Landing {
1158 carrier: ObjectId,
1159 extent: Option<LandingExtent>,
1160}
1161
1162impl Landing {
1163 #[must_use]
1165 pub fn new(carrier: ObjectId, extent: Option<LandingExtent>) -> Self {
1166 Self { carrier, extent }
1167 }
1168
1169 #[must_use]
1172 pub fn carrier(&self) -> &ObjectId {
1173 &self.carrier
1174 }
1175
1176 #[must_use]
1179 pub fn extent(&self) -> Option<LandingExtent> {
1180 self.extent
1181 }
1182}
1183
1184#[derive(Clone, Debug, PartialEq)]
1194pub struct LandingEvidence {
1195 request: LandingRequest,
1196 direction: MetricDirection,
1197 edge: ElevationInterval,
1198 landing: Option<Landing>,
1199 evidence: Evidence,
1200}
1201
1202impl LandingEvidence {
1203 pub fn try_new(
1208 request: LandingRequest,
1209 direction: MetricDirection,
1210 edge: ElevationInterval,
1211 landing: Option<Landing>,
1212 evidence: Evidence,
1213 ) -> Result<Self, WalkingSurfaceError> {
1214 #[allow(clippy::float_cmp)]
1215 if direction.components()[2] != 0.0 {
1216 return Err(WalkingSurfaceError::InvalidMeasurement);
1217 }
1218 if let Some(landing) = &landing {
1219 let named = landing.carrier == request.subject
1220 || request.candidates.binary_search(&landing.carrier).is_ok();
1221 let beyond = landing
1222 .extent
1223 .is_none_or(|extent| extent.far.lower_metres() > edge.upper_metres());
1224 if !named || !beyond {
1225 return Err(WalkingSurfaceError::InvalidMeasurement);
1226 }
1227 }
1228 let exact = edge.is_exact()
1229 && landing
1230 .as_ref()
1231 .and_then(|landing| landing.extent)
1232 .is_none_or(|extent| extent.is_exact());
1233 if evidence.exact != exact || evidence.locator.trim().is_empty() {
1234 return Err(WalkingSurfaceError::InexactEvidence);
1235 }
1236 Ok(Self {
1237 request,
1238 direction,
1239 edge,
1240 landing,
1241 evidence,
1242 })
1243 }
1244
1245 #[must_use]
1247 pub fn request(&self) -> &LandingRequest {
1248 &self.request
1249 }
1250
1251 #[must_use]
1253 pub fn direction(&self) -> MetricDirection {
1254 self.direction
1255 }
1256
1257 #[must_use]
1259 pub fn edge(&self) -> ElevationInterval {
1260 self.edge
1261 }
1262
1263 #[must_use]
1265 pub fn landing(&self) -> Option<&Landing> {
1266 self.landing.as_ref()
1267 }
1268
1269 #[must_use]
1272 pub fn depth(&self) -> Option<MeasuredInterval> {
1273 let extent = self.landing.as_ref()?.extent?;
1274 Some(between(extent.far, self.edge))
1275 }
1276
1277 #[must_use]
1280 pub fn width(&self) -> Option<MeasuredInterval> {
1281 let extent = self.landing.as_ref()?.extent?;
1282 Some(between(extent.right, extent.left))
1283 }
1284
1285 #[must_use]
1287 pub fn evidence(&self) -> &Evidence {
1288 &self.evidence
1289 }
1290}
1291
1292#[derive(Clone, Debug, PartialEq, Eq)]
1298pub struct ClearanceBelowRequest {
1299 subject: ObjectId,
1300 spaces: Vec<ObjectId>,
1301}
1302
1303impl ClearanceBelowRequest {
1304 #[must_use]
1306 pub fn new(subject: ObjectId, spaces: impl IntoIterator<Item = ObjectId>) -> Self {
1307 let mut spaces: Vec<ObjectId> = spaces
1308 .into_iter()
1309 .filter(|space| *space != subject)
1310 .collect();
1311 spaces.sort();
1312 spaces.dedup();
1313 Self { subject, spaces }
1314 }
1315
1316 #[must_use]
1318 pub fn subject(&self) -> &ObjectId {
1319 &self.subject
1320 }
1321
1322 #[must_use]
1324 pub fn spaces(&self) -> &[ObjectId] {
1325 &self.spaces
1326 }
1327}
1328
1329#[derive(Clone, Debug, PartialEq)]
1337pub struct ClearanceBelow {
1338 request: ClearanceBelowRequest,
1339 clearance: Option<MeasuredInterval>,
1340 governing: Vec<ObjectId>,
1341 evidence: Evidence,
1342}
1343
1344impl ClearanceBelow {
1345 pub fn try_new(
1350 request: ClearanceBelowRequest,
1351 clearance: Option<MeasuredInterval>,
1352 governing: Vec<ObjectId>,
1353 evidence: Evidence,
1354 ) -> Result<Self, WalkingSurfaceError> {
1355 let governing = governed(&request.spaces, clearance, governing, &evidence)?;
1356 Ok(Self {
1357 request,
1358 clearance,
1359 governing,
1360 evidence,
1361 })
1362 }
1363
1364 #[must_use]
1366 pub fn request(&self) -> &ClearanceBelowRequest {
1367 &self.request
1368 }
1369
1370 #[must_use]
1373 pub fn clearance(&self) -> Option<MeasuredInterval> {
1374 self.clearance
1375 }
1376
1377 #[must_use]
1379 pub fn governing(&self) -> &[ObjectId] {
1380 &self.governing
1381 }
1382
1383 #[must_use]
1385 pub fn evidence(&self) -> &Evidence {
1386 &self.evidence
1387 }
1388}
1389
1390fn governed(
1393 named: &[ObjectId],
1394 clearance: Option<MeasuredInterval>,
1395 mut governing: Vec<ObjectId>,
1396 evidence: &Evidence,
1397) -> Result<Vec<ObjectId>, WalkingSurfaceError> {
1398 governing.sort();
1399 governing.dedup();
1400 let requested = governing
1401 .iter()
1402 .all(|object| named.binary_search(object).is_ok());
1403 if !requested || clearance.is_some() == governing.is_empty() {
1404 return Err(WalkingSurfaceError::InvalidMeasurement);
1405 }
1406 if clearance.is_some_and(|clearance| clearance.lower < 0.0) {
1407 return Err(WalkingSurfaceError::InvalidMeasurement);
1408 }
1409 if evidence.locator.trim().is_empty()
1410 || (evidence.exact && clearance.is_some_and(|clearance| !clearance.is_point()))
1411 {
1412 return Err(WalkingSurfaceError::InexactEvidence);
1413 }
1414 Ok(governing)
1415}
1416
1417#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1420pub enum WalkingStretch {
1421 Flight,
1424 Run(usize),
1426}
1427
1428#[derive(Clone, Debug, PartialEq)]
1438pub struct HandrailRequest {
1439 subject: ObjectId,
1440 stretch: WalkingStretch,
1441 rails: Vec<ObjectId>,
1442 reach: f64,
1443 above: f64,
1444 extension: f64,
1445}
1446
1447impl HandrailRequest {
1448 pub fn try_new(
1451 subject: ObjectId,
1452 stretch: WalkingStretch,
1453 rails: impl IntoIterator<Item = ObjectId>,
1454 (reach, above): (f64, f64),
1455 extension: f64,
1456 ) -> Result<Self, WalkingSurfaceError> {
1457 if [reach, above, extension]
1458 .iter()
1459 .any(|length| !length.is_finite() || *length < 0.0)
1460 {
1461 return Err(WalkingSurfaceError::InvalidMeasurement);
1462 }
1463 let mut rails: Vec<ObjectId> = rails.into_iter().filter(|rail| *rail != subject).collect();
1464 rails.sort();
1465 rails.dedup();
1466 Ok(Self {
1467 subject,
1468 stretch,
1469 rails,
1470 reach,
1471 above,
1472 extension,
1473 })
1474 }
1475
1476 #[must_use]
1478 pub fn subject(&self) -> &ObjectId {
1479 &self.subject
1480 }
1481
1482 #[must_use]
1484 pub fn stretch(&self) -> WalkingStretch {
1485 self.stretch
1486 }
1487
1488 #[must_use]
1490 pub fn rails(&self) -> &[ObjectId] {
1491 &self.rails
1492 }
1493
1494 #[must_use]
1496 pub fn reach(&self) -> f64 {
1497 self.reach
1498 }
1499
1500 #[must_use]
1503 pub fn above(&self) -> f64 {
1504 self.above
1505 }
1506
1507 #[must_use]
1509 pub fn extension(&self) -> f64 {
1510 self.extension
1511 }
1512}
1513
1514#[derive(Clone, Debug, PartialEq)]
1524pub struct ClearWidthRequest {
1525 subject: ObjectId,
1526 stretch: WalkingStretch,
1527 obstacles: Vec<ObjectId>,
1528 band: (f64, f64),
1529}
1530
1531impl ClearWidthRequest {
1532 pub fn try_new(
1535 subject: ObjectId,
1536 stretch: WalkingStretch,
1537 obstacles: impl IntoIterator<Item = ObjectId>,
1538 band: (f64, f64),
1539 ) -> Result<Self, WalkingSurfaceError> {
1540 let (from, to) = band;
1541 if !from.is_finite() || !to.is_finite() || from < 0.0 || to <= from {
1542 return Err(WalkingSurfaceError::InvalidMeasurement);
1543 }
1544 let mut obstacles: Vec<ObjectId> = obstacles
1545 .into_iter()
1546 .filter(|obstacle| *obstacle != subject)
1547 .collect();
1548 obstacles.sort();
1549 obstacles.dedup();
1550 Ok(Self {
1551 subject,
1552 stretch,
1553 obstacles,
1554 band,
1555 })
1556 }
1557
1558 #[must_use]
1560 pub fn subject(&self) -> &ObjectId {
1561 &self.subject
1562 }
1563
1564 #[must_use]
1566 pub fn stretch(&self) -> WalkingStretch {
1567 self.stretch
1568 }
1569
1570 #[must_use]
1572 pub fn obstacles(&self) -> &[ObjectId] {
1573 &self.obstacles
1574 }
1575
1576 #[must_use]
1578 pub fn band(&self) -> (f64, f64) {
1579 self.band
1580 }
1581}
1582
1583#[derive(Clone, Debug, PartialEq)]
1592pub struct ClearWidthEvidence {
1593 request: ClearWidthRequest,
1594 width: MeasuredInterval,
1595 governing: Vec<ObjectId>,
1596 evidence: Evidence,
1597}
1598
1599impl ClearWidthEvidence {
1600 pub fn try_new(
1603 request: ClearWidthRequest,
1604 width: MeasuredInterval,
1605 mut governing: Vec<ObjectId>,
1606 evidence: Evidence,
1607 ) -> Result<Self, WalkingSurfaceError> {
1608 governing.sort();
1609 governing.dedup();
1610 if width.lower < 0.0
1611 || !governing
1612 .iter()
1613 .all(|object| request.obstacles.binary_search(object).is_ok())
1614 {
1615 return Err(WalkingSurfaceError::InvalidMeasurement);
1616 }
1617 if evidence.locator.trim().is_empty() || (evidence.exact && !width.is_point()) {
1618 return Err(WalkingSurfaceError::InexactEvidence);
1619 }
1620 Ok(Self {
1621 request,
1622 width,
1623 governing,
1624 evidence,
1625 })
1626 }
1627
1628 #[must_use]
1630 pub fn request(&self) -> &ClearWidthRequest {
1631 &self.request
1632 }
1633
1634 #[must_use]
1636 pub fn width(&self) -> MeasuredInterval {
1637 self.width
1638 }
1639
1640 #[must_use]
1642 pub fn governing(&self) -> &[ObjectId] {
1643 &self.governing
1644 }
1645
1646 #[must_use]
1648 pub fn evidence(&self) -> &Evidence {
1649 &self.evidence
1650 }
1651}
1652
1653#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1657pub enum RailSide {
1658 Right,
1660 Left,
1662}
1663
1664#[derive(Clone, Copy, Debug, PartialEq)]
1673pub struct StretchPart {
1674 direction: MetricDirection,
1675 sides: (ElevationInterval, ElevationInterval),
1676}
1677
1678impl StretchPart {
1679 pub fn try_new(
1682 direction: MetricDirection,
1683 (left, right): (ElevationInterval, ElevationInterval),
1684 ) -> Result<Self, WalkingSurfaceError> {
1685 #[allow(clippy::float_cmp)]
1686 if direction.components()[2] != 0.0 {
1687 return Err(WalkingSurfaceError::InvalidMeasurement);
1688 }
1689 Ok(Self {
1690 direction,
1691 sides: sides(left, right)?,
1692 })
1693 }
1694
1695 #[must_use]
1697 pub fn direction(&self) -> MetricDirection {
1698 self.direction
1699 }
1700
1701 #[must_use]
1703 pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1704 self.sides
1705 }
1706}
1707
1708#[derive(Clone, Copy, Debug, PartialEq)]
1722pub struct RailMeasurement {
1723 part: usize,
1724 start: ElevationInterval,
1725 end: ElevationInterval,
1726 left: ElevationInterval,
1727 right: ElevationInterval,
1728 lowest: MeasuredInterval,
1729 highest: MeasuredInterval,
1730 bottom_rise: Option<MeasuredInterval>,
1731 top_rise: Option<MeasuredInterval>,
1732}
1733
1734impl RailMeasurement {
1735 pub fn try_new(
1739 (start, end): (ElevationInterval, ElevationInterval),
1740 (left, right): (ElevationInterval, ElevationInterval),
1741 lowest: MeasuredInterval,
1742 highest: MeasuredInterval,
1743 ) -> Result<Self, WalkingSurfaceError> {
1744 let (start, end) = sides(start, end)?;
1745 let (left, right) = sides(left, right)?;
1746 if lowest.lower > highest.lower || lowest.upper > highest.upper {
1747 return Err(WalkingSurfaceError::InvalidMeasurement);
1748 }
1749 Ok(Self {
1750 part: 0,
1751 start,
1752 end,
1753 left,
1754 right,
1755 lowest,
1756 highest,
1757 bottom_rise: None,
1758 top_rise: None,
1759 })
1760 }
1761
1762 #[must_use]
1765 pub fn in_part(mut self, part: usize) -> Self {
1766 self.part = part;
1767 self
1768 }
1769
1770 #[must_use]
1772 pub fn part(&self) -> usize {
1773 self.part
1774 }
1775
1776 pub fn with_rises(
1779 mut self,
1780 bottom: Option<MeasuredInterval>,
1781 top: Option<MeasuredInterval>,
1782 ) -> Result<Self, WalkingSurfaceError> {
1783 if [bottom, top].iter().flatten().any(|rise| rise.lower < 0.0) {
1784 return Err(WalkingSurfaceError::InvalidMeasurement);
1785 }
1786 self.bottom_rise = bottom;
1787 self.top_rise = top;
1788 Ok(self)
1789 }
1790
1791 #[must_use]
1793 pub fn start(&self) -> ElevationInterval {
1794 self.start
1795 }
1796
1797 #[must_use]
1799 pub fn end(&self) -> ElevationInterval {
1800 self.end
1801 }
1802
1803 #[must_use]
1805 pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1806 (self.left, self.right)
1807 }
1808
1809 #[must_use]
1811 pub fn lowest(&self) -> MeasuredInterval {
1812 self.lowest
1813 }
1814
1815 #[must_use]
1817 pub fn highest(&self) -> MeasuredInterval {
1818 self.highest
1819 }
1820
1821 #[must_use]
1824 pub fn bottom_rise(&self) -> Option<MeasuredInterval> {
1825 self.bottom_rise
1826 }
1827
1828 #[must_use]
1830 pub fn top_rise(&self) -> Option<MeasuredInterval> {
1831 self.top_rise
1832 }
1833
1834 fn plan(&self, direction: MetricDirection, outer: bool) -> Option<ConvexPlanRegion> {
1838 let ((near, far), (low, high)) = if outer {
1839 (
1840 (self.start.lower_metres(), self.end.upper_metres()),
1841 (self.left.lower_metres(), self.right.upper_metres()),
1842 )
1843 } else {
1844 (
1845 (self.start.upper_metres(), self.end.lower_metres()),
1846 (self.left.upper_metres(), self.right.lower_metres()),
1847 )
1848 };
1849 if near >= far || low >= high {
1850 return None;
1851 }
1852 let [dx, dy, _] = direction.components();
1853 let [ax, ay, _] = across(direction).components();
1854 let at = |along: f64, beside: f64| {
1855 [
1856 along.mul_add(dx, beside * ax),
1857 along.mul_add(dy, beside * ay),
1858 ]
1859 };
1860 ConvexPlanRegion::try_new(vec![
1862 at(near, low),
1863 at(far, low),
1864 at(far, high),
1865 at(near, high),
1866 ])
1867 .ok()
1868 }
1869}
1870
1871#[derive(Clone, Debug, PartialEq)]
1884pub struct HandrailEvidence {
1885 request: HandrailRequest,
1886 parts: Vec<StretchPart>,
1887 pitch: (ElevationInterval, ElevationInterval),
1888 rails: Vec<(ObjectId, RailMeasurement)>,
1889 evidence: Evidence,
1890}
1891
1892impl HandrailEvidence {
1893 pub fn try_new(
1898 request: HandrailRequest,
1899 direction: MetricDirection,
1900 pitch: (ElevationInterval, ElevationInterval),
1901 walking_sides: (ElevationInterval, ElevationInterval),
1902 rails: Vec<(ObjectId, RailMeasurement)>,
1903 evidence: Evidence,
1904 ) -> Result<Self, WalkingSurfaceError> {
1905 let part = StretchPart::try_new(direction, walking_sides)?;
1906 Self::try_in_parts(request, vec![part], pitch, rails, evidence)
1907 }
1908
1909 pub fn try_in_parts(
1913 request: HandrailRequest,
1914 parts: Vec<StretchPart>,
1915 pitch: (ElevationInterval, ElevationInterval),
1916 mut rails: Vec<(ObjectId, RailMeasurement)>,
1917 evidence: Evidence,
1918 ) -> Result<Self, WalkingSurfaceError> {
1919 if parts.is_empty() {
1920 return Err(WalkingSurfaceError::InvalidMeasurement);
1921 }
1922 if parts.len() == 1 {
1923 sides(pitch.0, pitch.1)?;
1925 }
1926 rails.sort_by(|a, b| a.0.cmp(&b.0));
1927 let unique = rails.windows(2).all(|pair| pair[0].0 != pair[1].0);
1928 let requested = rails.iter().all(|(rail, measurement)| {
1929 request.rails.binary_search(rail).is_ok() && measurement.part < parts.len()
1930 });
1931 if !unique || !requested {
1932 return Err(WalkingSurfaceError::InvalidMeasurement);
1933 }
1934 if evidence.exact || evidence.locator.trim().is_empty() {
1935 return Err(WalkingSurfaceError::InexactEvidence);
1936 }
1937 Ok(Self {
1938 request,
1939 parts,
1940 pitch,
1941 rails,
1942 evidence,
1943 })
1944 }
1945
1946 #[must_use]
1948 pub fn request(&self) -> &HandrailRequest {
1949 &self.request
1950 }
1951
1952 #[must_use]
1954 pub fn parts(&self) -> &[StretchPart] {
1955 &self.parts
1956 }
1957
1958 #[must_use]
1961 pub fn direction(&self) -> MetricDirection {
1962 self.first().direction
1963 }
1964
1965 #[must_use]
1968 pub fn pitch(&self) -> (ElevationInterval, ElevationInterval) {
1969 self.pitch
1970 }
1971
1972 #[must_use]
1975 pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1976 self.first().sides
1977 }
1978
1979 fn first(&self) -> &StretchPart {
1980 &self.parts[0]
1981 }
1982
1983 fn part(&self, rail: &RailMeasurement) -> &StretchPart {
1984 &self.parts[rail.part]
1985 }
1986
1987 #[must_use]
1989 pub fn rails(&self) -> &[(ObjectId, RailMeasurement)] {
1990 &self.rails
1991 }
1992
1993 #[must_use]
1998 pub fn bottom_extension(&self, rail: &RailMeasurement) -> Option<MeasuredInterval> {
1999 (rail.part == 0).then(|| between(self.pitch.0, rail.start))
2000 }
2001
2002 #[must_use]
2006 pub fn top_extension(&self, rail: &RailMeasurement) -> Option<MeasuredInterval> {
2007 (rail.part + 1 == self.parts.len()).then(|| between(rail.end, self.pitch.1))
2008 }
2009
2010 pub fn side_rail(
2019 &self,
2020 side: RailSide,
2021 ) -> Result<Vec<&(ObjectId, RailMeasurement)>, Vec<ObjectId>> {
2022 let mut pieces: Vec<&(ObjectId, RailMeasurement)> = self
2023 .rails
2024 .iter()
2025 .filter(|(_, rail)| self.side(rail) == Some(side))
2026 .collect();
2027 pieces.sort_by(|a, b| {
2028 a.1.part
2029 .cmp(&b.1.part)
2030 .then_with(|| {
2031 a.1.start
2032 .lower_metres()
2033 .total_cmp(&b.1.start.lower_metres())
2034 })
2035 .then_with(|| a.0.cmp(&b.0))
2036 });
2037 let ordered = pieces.windows(2).all(|pair| {
2038 let (lower, upper) = (&pair[0].1, &pair[1].1);
2039 lower.part < upper.part
2040 || (lower.start.upper_metres() < upper.start.lower_metres()
2041 && lower.end.upper_metres() < upper.end.lower_metres())
2042 });
2043 if ordered {
2044 Ok(pieces)
2045 } else {
2046 Err(pieces.into_iter().map(|(rail, _)| rail.clone()).collect())
2047 }
2048 }
2049
2050 #[must_use]
2056 pub fn gap(&self, a: &RailMeasurement, b: &RailMeasurement) -> Option<MeasuredInterval> {
2057 let (along_a, along_b) = (self.part(a).direction, self.part(b).direction);
2058 let (outer_a, inner_a) = (a.plan(along_a, true)?, a.plan(along_a, false));
2059 let (outer_b, inner_b) = (b.plan(along_b, true)?, b.plan(along_b, false));
2060 let (inner_a, inner_b) = (inner_a?, inner_b?);
2061 let scale = [&outer_a, &outer_b]
2062 .iter()
2063 .flat_map(|region| region.ring())
2064 .flatten()
2065 .fold(1.0_f64, |scale, value| scale.max(value.abs()));
2066 let margin = 64.0 * f64::EPSILON * scale;
2069 let lower = (outer_a.separation(&outer_b) - margin).max(0.0);
2070 let upper = inner_a.separation(&inner_b).max(0.0) + margin;
2071 MeasuredInterval::try_new(lower, upper.max(lower)).ok()
2072 }
2073
2074 #[must_use]
2078 pub fn side(&self, rail: &RailMeasurement) -> Option<RailSide> {
2079 let (left, right) = self.part(rail).sides;
2080 let low = f64::midpoint(left.lower_metres(), right.lower_metres()).next_down();
2083 let high = f64::midpoint(left.upper_metres(), right.upper_metres()).next_up();
2084 if rail.right.upper_metres() < low {
2085 Some(RailSide::Right)
2086 } else if rail.left.lower_metres() > high {
2087 Some(RailSide::Left)
2088 } else {
2089 None
2090 }
2091 }
2092
2093 #[must_use]
2095 pub fn evidence(&self) -> &Evidence {
2096 &self.evidence
2097 }
2098}
2099
2100pub trait WalkingSurfaceService: Send + Sync + 'static {
2102 fn measure_tread_flight(
2105 &self,
2106 request: &TreadFlightRequest,
2107 ) -> Result<TreadFlight, WalkingSurfaceError>;
2108
2109 fn measure_sloped_runs(&self, object: &ObjectId) -> Result<SlopedSurface, WalkingSurfaceError>;
2111
2112 fn measure_headroom(&self, request: &HeadroomRequest) -> Result<Headroom, WalkingSurfaceError>;
2114
2115 fn measure_landing(
2119 &self,
2120 request: &LandingRequest,
2121 ) -> Result<LandingEvidence, WalkingSurfaceError> {
2122 Err(WalkingSurfaceError::Unsupported(format!(
2123 "landings of {} are not measured by this service",
2124 request.subject()
2125 )))
2126 }
2127
2128 fn measure_clearance_below(
2131 &self,
2132 request: &ClearanceBelowRequest,
2133 ) -> Result<ClearanceBelow, WalkingSurfaceError> {
2134 Err(WalkingSurfaceError::Unsupported(format!(
2135 "the clearance below {} is not measured by this service",
2136 request.subject()
2137 )))
2138 }
2139
2140 fn measure_handrails(
2144 &self,
2145 request: &HandrailRequest,
2146 ) -> Result<HandrailEvidence, WalkingSurfaceError> {
2147 Err(WalkingSurfaceError::Unsupported(format!(
2148 "handrails along {} are not measured by this service",
2149 request.subject()
2150 )))
2151 }
2152
2153 fn measure_clear_width(
2157 &self,
2158 request: &ClearWidthRequest,
2159 ) -> Result<ClearWidthEvidence, WalkingSurfaceError> {
2160 Err(WalkingSurfaceError::Unsupported(format!(
2161 "the clear width along {} is not measured by this service",
2162 request.subject()
2163 )))
2164 }
2165}
2166
2167#[derive(Clone)]
2169pub struct WalkingSurfaceServiceHandle(Arc<dyn WalkingSurfaceService>);
2170
2171impl WalkingSurfaceServiceHandle {
2172 #[must_use]
2174 pub fn new(service: Arc<dyn WalkingSurfaceService>) -> Self {
2175 Self(service)
2176 }
2177
2178 pub fn measure_tread_flight(
2181 &self,
2182 request: &TreadFlightRequest,
2183 ) -> Result<TreadFlight, WalkingSurfaceError> {
2184 let flight = self.0.measure_tread_flight(request)?;
2185 if flight.request() != request {
2186 return Err(WalkingSurfaceError::InvalidMeasurement);
2187 }
2188 Ok(flight)
2189 }
2190
2191 pub fn measure_sloped_runs(
2193 &self,
2194 object: &ObjectId,
2195 ) -> Result<SlopedSurface, WalkingSurfaceError> {
2196 let ramp = self.0.measure_sloped_runs(object)?;
2197 if ramp.object() != object {
2198 return Err(WalkingSurfaceError::InvalidMeasurement);
2199 }
2200 Ok(ramp)
2201 }
2202
2203 pub fn measure_headroom(
2206 &self,
2207 request: &HeadroomRequest,
2208 ) -> Result<Headroom, WalkingSurfaceError> {
2209 let headroom = self.0.measure_headroom(request)?;
2210 if headroom.request() != request {
2211 return Err(WalkingSurfaceError::InvalidMeasurement);
2212 }
2213 Ok(headroom)
2214 }
2215
2216 pub fn measure_landing(
2219 &self,
2220 request: &LandingRequest,
2221 ) -> Result<LandingEvidence, WalkingSurfaceError> {
2222 let landing = self.0.measure_landing(request)?;
2223 if landing.request() != request {
2224 return Err(WalkingSurfaceError::InvalidMeasurement);
2225 }
2226 Ok(landing)
2227 }
2228
2229 pub fn measure_clearance_below(
2232 &self,
2233 request: &ClearanceBelowRequest,
2234 ) -> Result<ClearanceBelow, WalkingSurfaceError> {
2235 let below = self.0.measure_clearance_below(request)?;
2236 if below.request() != request {
2237 return Err(WalkingSurfaceError::InvalidMeasurement);
2238 }
2239 Ok(below)
2240 }
2241
2242 pub fn measure_handrails(
2245 &self,
2246 request: &HandrailRequest,
2247 ) -> Result<HandrailEvidence, WalkingSurfaceError> {
2248 let rails = self.0.measure_handrails(request)?;
2249 if rails.request() != request {
2250 return Err(WalkingSurfaceError::InvalidMeasurement);
2251 }
2252 Ok(rails)
2253 }
2254
2255 pub fn measure_clear_width(
2258 &self,
2259 request: &ClearWidthRequest,
2260 ) -> Result<ClearWidthEvidence, WalkingSurfaceError> {
2261 let width = self.0.measure_clear_width(request)?;
2262 if width.request() != request {
2263 return Err(WalkingSurfaceError::InvalidMeasurement);
2264 }
2265 Ok(width)
2266 }
2267}
2268
2269#[cfg(test)]
2270mod tests {
2271 use super::*;
2272 use axioval_ir::SourceId;
2273
2274 fn source() -> SourceId {
2275 SourceId::new("cad", "m").unwrap()
2276 }
2277
2278 fn id(local: &str) -> ObjectId {
2279 ObjectId::new(source(), local).unwrap()
2280 }
2281
2282 fn point(value: f64) -> ElevationInterval {
2283 ElevationInterval::exact(value).unwrap()
2284 }
2285
2286 fn request(local: &str) -> TreadFlightRequest {
2287 TreadFlightRequest::new(id(local))
2288 }
2289
2290 fn x() -> MetricDirection {
2291 MetricDirection::try_new([1.0, 0.0, 0.0]).unwrap()
2292 }
2293
2294 fn tread(z: f64, front: f64, back: f64) -> Tread {
2295 Tread::try_new(point(z), point(front), point(back)).unwrap()
2296 }
2297
2298 fn evidence(exact: bool) -> Evidence {
2299 Evidence {
2300 source: source(),
2301 locator: "tread-flight:a".into(),
2302 exact,
2303 }
2304 }
2305
2306 fn contains(interval: MeasuredInterval, value: f64) -> bool {
2309 interval.lower() - 1e-15 <= value && value <= interval.upper() + 1e-15
2310 }
2311
2312 #[test]
2313 fn risers_goings_and_nosings_come_from_the_positions() {
2314 let treads = vec![
2315 tread(0.17, 0.0, 0.30),
2316 tread(0.34, 0.28, 0.58),
2317 tread(0.55, 0.56, 0.86),
2318 ];
2319 let flight = TreadFlight::try_new(
2320 request("a"),
2321 WalkingLine::Straight(x()),
2322 point(0.0),
2323 point(0.72),
2324 treads,
2325 evidence(true),
2326 )
2327 .unwrap();
2328 assert!(flight.ends_in_riser());
2329 let risers = flight.risers();
2330 assert_eq!(risers.len(), 4);
2331 let closures = flight
2333 .clone()
2334 .with_final_riser(RiserClosure::Open)
2335 .riser_closures();
2336 assert_eq!(closures.len(), 4);
2337 assert_eq!(closures[3], RiserClosure::Open);
2338 assert_eq!(flight.riser_closures()[3], RiserClosure::NotMeasured);
2339 for (riser, expected) in risers.iter().zip([0.17, 0.17, 0.21, 0.17]) {
2340 assert!(contains(*riser, expected), "{riser:?} {expected}");
2341 assert!(riser.upper() - riser.lower() <= 2.0 * f64::EPSILON);
2342 }
2343 let goings = flight.goings();
2344 assert_eq!(goings.len(), 2);
2345 assert!(goings.iter().all(|going| contains(*going, 0.28)));
2346 assert!(
2347 flight
2348 .nosings()
2349 .iter()
2350 .all(|nosing| contains(*nosing, 0.02))
2351 );
2352 assert!(contains(flight.rise(), 0.72));
2353 assert!(contains(flight.treads()[0].depth(), 0.30));
2354 }
2355
2356 #[test]
2357 fn a_flight_whose_top_is_its_last_tread_has_no_final_riser() {
2358 let treads = vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
2359 let flight = TreadFlight::try_new(
2360 request("a"),
2361 WalkingLine::Straight(x()),
2362 point(0.0),
2363 point(0.4),
2364 treads,
2365 evidence(true),
2366 )
2367 .unwrap();
2368 assert!(!flight.ends_in_riser());
2369 assert_eq!(flight.risers().len(), 2);
2370 assert!(contains(flight.rise(), 0.4));
2371 }
2372
2373 #[test]
2374 fn incoherent_flights_are_refused() {
2375 let ordered = || vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
2376 let reversed = vec![tread(0.4, 0.3, 0.6), tread(0.2, 0.0, 0.3)];
2378 assert_eq!(
2379 TreadFlight::try_new(
2380 request("a"),
2381 WalkingLine::Straight(x()),
2382 point(0.0),
2383 point(0.4),
2384 reversed,
2385 evidence(true)
2386 ),
2387 Err(WalkingSurfaceError::InvalidMeasurement)
2388 );
2389 assert_eq!(
2391 TreadFlight::try_new(
2392 request("a"),
2393 WalkingLine::Straight(x()),
2394 point(0.3),
2395 point(0.4),
2396 ordered(),
2397 evidence(true)
2398 ),
2399 Err(WalkingSurfaceError::InvalidMeasurement)
2400 );
2401 assert_eq!(
2402 TreadFlight::try_new(
2403 request("a"),
2404 WalkingLine::Straight(x()),
2405 point(0.0),
2406 point(0.3),
2407 ordered(),
2408 evidence(true)
2409 ),
2410 Err(WalkingSurfaceError::InvalidMeasurement)
2411 );
2412 assert_eq!(
2414 TreadFlight::try_new(
2415 request("a"),
2416 WalkingLine::Straight(x()),
2417 point(0.0),
2418 point(0.4),
2419 vec![],
2420 evidence(true)
2421 ),
2422 Err(WalkingSurfaceError::InvalidMeasurement)
2423 );
2424 let sloped = MetricDirection::try_new([1.0, 0.0, 1.0]).unwrap();
2425 assert_eq!(
2426 TreadFlight::try_new(
2427 request("a"),
2428 WalkingLine::Straight(sloped),
2429 point(0.0),
2430 point(0.4),
2431 ordered(),
2432 evidence(true)
2433 ),
2434 Err(WalkingSurfaceError::InvalidMeasurement)
2435 );
2436 assert_eq!(
2438 TreadFlight::try_new(
2439 request("a"),
2440 WalkingLine::Straight(x()),
2441 point(0.0),
2442 point(0.4),
2443 ordered(),
2444 evidence(false)
2445 ),
2446 Err(WalkingSurfaceError::InexactEvidence)
2447 );
2448 let widened = ElevationInterval::try_new(0.39, 0.41).unwrap();
2449 assert_eq!(
2450 TreadFlight::try_new(
2451 request("a"),
2452 WalkingLine::Straight(x()),
2453 point(0.0),
2454 widened,
2455 ordered(),
2456 evidence(true)
2457 ),
2458 Err(WalkingSurfaceError::InvalidMeasurement)
2459 );
2460 }
2461
2462 #[test]
2463 fn a_run_slope_is_its_rise_over_its_length() {
2464 let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(1.0), point(7.0)).unwrap();
2465 assert!(contains(run.slope(), 0.5 / 6.0));
2466 assert!(run.slope().upper() - run.slope().lower() <= 4.0 * f64::EPSILON);
2467 assert!(contains(run.length(), 6.0) && contains(run.rise(), 0.5));
2468 assert_eq!(
2469 SlopedRun::try_new(x(), point(0.5), point(0.5), point(1.0), point(7.0)),
2470 Err(WalkingSurfaceError::InvalidMeasurement)
2471 );
2472 let ramp = SlopedSurface::try_new(id("a"), vec![run], evidence(true)).unwrap();
2473 assert!(ramp.is_exact());
2474 assert_eq!(
2475 SlopedSurface::try_new(id("a"), vec![], evidence(true)),
2476 Err(WalkingSurfaceError::InvalidMeasurement)
2477 );
2478 }
2479
2480 #[test]
2481 fn headroom_names_requested_obstacles_only() {
2482 let request = HeadroomRequest::new(id("a"), [id("c"), id("a"), id("b"), id("c")]);
2483 assert_eq!(request.obstacles(), &[id("b"), id("c")]);
2484 let clearance = MeasuredInterval::try_new(2.0, 2.0 + 1e-9).ok();
2485 assert!(
2486 Headroom::try_new(request.clone(), clearance, vec![id("b")], evidence(false)).is_ok()
2487 );
2488 assert_eq!(
2489 Headroom::try_new(request.clone(), clearance, vec![id("d")], evidence(false)),
2490 Err(WalkingSurfaceError::InvalidMeasurement)
2491 );
2492 assert_eq!(
2493 Headroom::try_new(request.clone(), clearance, vec![], evidence(false)),
2494 Err(WalkingSurfaceError::InvalidMeasurement)
2495 );
2496 assert_eq!(
2497 Headroom::try_new(request.clone(), clearance, vec![id("b")], evidence(true)),
2498 Err(WalkingSurfaceError::InexactEvidence)
2499 );
2500 assert!(Headroom::try_new(request, None, vec![], evidence(false)).is_ok());
2501 }
2502
2503 struct Other;
2504 impl WalkingSurfaceService for Other {
2505 fn measure_tread_flight(
2506 &self,
2507 _: &TreadFlightRequest,
2508 ) -> Result<TreadFlight, WalkingSurfaceError> {
2509 TreadFlight::try_new(
2510 request("b"),
2511 WalkingLine::Straight(x()),
2512 point(0.0),
2513 point(0.2),
2514 vec![tread(0.2, 0.0, 0.3)],
2515 evidence(true),
2516 )
2517 }
2518 fn measure_sloped_runs(&self, _: &ObjectId) -> Result<SlopedSurface, WalkingSurfaceError> {
2519 let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(0.0), point(6.0))?;
2520 SlopedSurface::try_new(id("b"), vec![run], evidence(true))
2521 }
2522 fn measure_headroom(&self, _: &HeadroomRequest) -> Result<Headroom, WalkingSurfaceError> {
2523 Headroom::try_new(
2524 HeadroomRequest::new(id("b"), []),
2525 None,
2526 vec![],
2527 evidence(false),
2528 )
2529 }
2530 }
2531
2532 #[test]
2533 fn answers_about_another_object_are_refused() {
2534 let handle = WalkingSurfaceServiceHandle::new(Arc::new(Other));
2535 assert_eq!(
2536 handle.measure_tread_flight(&request("a")),
2537 Err(WalkingSurfaceError::InvalidMeasurement)
2538 );
2539 assert_eq!(
2540 handle.measure_sloped_runs(&id("a")),
2541 Err(WalkingSurfaceError::InvalidMeasurement)
2542 );
2543 assert_eq!(
2544 handle.measure_headroom(&HeadroomRequest::new(id("a"), [])),
2545 Err(WalkingSurfaceError::InvalidMeasurement)
2546 );
2547 assert!(handle.measure_tread_flight(&request("b")).is_ok());
2548 assert!(matches!(
2551 handle.measure_landing(&LandingRequest::new(id("b"), WalkingEnd::FlightTop, [])),
2552 Err(WalkingSurfaceError::Unsupported(_))
2553 ));
2554 assert!(matches!(
2555 handle.measure_clearance_below(&ClearanceBelowRequest::new(id("b"), [])),
2556 Err(WalkingSurfaceError::Unsupported(_))
2557 ));
2558 let rails =
2559 HandrailRequest::try_new(id("b"), WalkingStretch::Flight, [], (0.1, 1.5), 0.3).unwrap();
2560 assert!(matches!(
2561 handle.measure_handrails(&rails),
2562 Err(WalkingSurfaceError::Unsupported(_))
2563 ));
2564 }
2565
2566 fn rail(left: f64, right: f64, lowest: f64) -> RailMeasurement {
2567 RailMeasurement::try_new(
2568 (point(-0.3), point(1.5)),
2569 (point(left), point(right)),
2570 MeasuredInterval::try_new(lowest, lowest + 1e-9).unwrap(),
2571 MeasuredInterval::try_new(lowest + 0.01, lowest + 0.01 + 1e-9).unwrap(),
2572 )
2573 .unwrap()
2574 }
2575
2576 #[test]
2577 fn handrails_are_requested_rails_on_a_side_with_extensions() {
2578 let request = HandrailRequest::try_new(
2579 id("a"),
2580 WalkingStretch::Flight,
2581 [id("r"), id("a"), id("l"), id("r")],
2582 (0.2, 1.5),
2583 0.3,
2584 )
2585 .unwrap();
2586 assert_eq!(request.rails(), &[id("l"), id("r")]);
2587 assert!(
2588 HandrailRequest::try_new(id("a"), WalkingStretch::Flight, [], (-0.1, 1.5), 0.0)
2589 .is_err()
2590 );
2591 let evidence = |exact: bool| Evidence {
2592 source: source(),
2593 locator: "handrails:a".into(),
2594 exact,
2595 };
2596 let measure = |rails: Vec<(ObjectId, RailMeasurement)>, exact: bool| {
2597 HandrailEvidence::try_new(
2598 request.clone(),
2599 x(),
2600 (point(0.0), point(1.12)),
2601 (point(0.0), point(1.2)),
2602 rails,
2603 evidence(exact),
2604 )
2605 };
2606 let rails = vec![
2607 (id("r"), rail(-0.05, 0.0, 0.9)),
2608 (id("l"), rail(1.2, 1.25, 0.85)),
2609 ];
2610 let measured = measure(rails.clone(), false).unwrap();
2611 assert_eq!(measured.rails()[0].0, id("l"));
2612 let (left, right) = (measured.rails()[0].1, measured.rails()[1].1);
2613 assert_eq!(measured.side(&left), Some(RailSide::Left));
2614 assert_eq!(measured.side(&right), Some(RailSide::Right));
2615 assert_eq!(measured.side(&rail(0.5, 0.7, 0.9)), None);
2616 assert!(contains(measured.bottom_extension(&left).unwrap(), 0.3));
2617 assert!(contains(measured.top_extension(&left).unwrap(), 0.38));
2618 assert_eq!(
2621 measure(rails, true),
2622 Err(WalkingSurfaceError::InexactEvidence)
2623 );
2624 for rails in [
2625 vec![(id("x"), rail(0.0, 0.1, 0.9))],
2626 vec![
2627 (id("l"), rail(0.0, 0.1, 0.9)),
2628 (id("l"), rail(0.0, 0.1, 0.9)),
2629 ],
2630 ] {
2631 assert_eq!(
2632 measure(rails, false),
2633 Err(WalkingSurfaceError::InvalidMeasurement)
2634 );
2635 }
2636 let rise = MeasuredInterval::try_new(-0.1, 0.0).ok();
2638 assert!(rail(0.0, 0.1, 0.9).with_rises(rise, None).is_err());
2639 assert!(
2640 RailMeasurement::try_new(
2641 (point(0.0), point(1.0)),
2642 (point(0.0), point(0.1)),
2643 MeasuredInterval::try_new(1.0, 1.0).unwrap(),
2644 MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2645 )
2646 .is_err()
2647 );
2648 }
2649
2650 fn piece(start: f64, end: f64, left: f64, right: f64) -> RailMeasurement {
2651 RailMeasurement::try_new(
2652 (point(start), point(end)),
2653 (point(left), point(right)),
2654 MeasuredInterval::try_new(0.9, 0.9 + 1e-9).unwrap(),
2655 MeasuredInterval::try_new(0.9, 0.9 + 1e-9).unwrap(),
2656 )
2657 .unwrap()
2658 }
2659
2660 #[test]
2661 fn the_pieces_along_a_side_are_ordered_with_their_gaps() {
2662 let request = HandrailRequest::try_new(
2663 id("a"),
2664 WalkingStretch::Flight,
2665 [id("p"), id("q"), id("r"), id("s"), id("m")],
2666 (0.2, 1.5),
2667 0.3,
2668 )
2669 .unwrap();
2670 let measure = |rails: Vec<(ObjectId, RailMeasurement)>| {
2671 HandrailEvidence::try_new(
2672 request.clone(),
2673 x(),
2674 (point(0.0), point(0.84)),
2675 (point(0.0), point(1.2)),
2676 rails,
2677 evidence(false),
2678 )
2679 .unwrap()
2680 };
2681 let measured = measure(vec![
2684 (id("p"), piece(0.5, 1.14, 1.25, 1.3)),
2685 (id("q"), piece(-0.3, 0.4, 1.25, 1.3)),
2686 (id("r"), piece(-0.3, 0.4, -0.1, -0.05)),
2687 (id("s"), piece(0.4, 1.14, -0.1, -0.05)),
2688 (id("m"), piece(-0.3, 1.14, 0.55, 0.65)),
2689 ]);
2690 let left = measured.side_rail(RailSide::Left).unwrap();
2691 let names: Vec<&ObjectId> = left.iter().map(|(rail, _)| rail).collect();
2692 assert_eq!(names, [&id("q"), &id("p")]);
2693 let gap = measured.gap(&left[0].1, &left[1].1).unwrap();
2694 assert!(
2695 contains(gap, 0.1) && gap.upper() - gap.lower() < 1e-12,
2696 "{gap:?}"
2697 );
2698 let right = measured.side_rail(RailSide::Right).unwrap();
2699 let gap = measured.gap(&right[0].1, &right[1].1).unwrap();
2700 assert!(gap.lower() == 0.0 && gap.upper() < 1e-12, "{gap:?}");
2701 let apart = measured
2703 .gap(&piece(-0.3, 0.4, 1.25, 1.3), &piece(0.7, 1.14, 1.6, 1.65))
2704 .unwrap();
2705 assert!(contains(apart, 0.3_f64.hypot(0.3)), "{apart:?}");
2706 let nested = measure(vec![
2708 (id("p"), piece(-0.3, 1.14, 1.25, 1.3)),
2709 (id("q"), piece(0.0, 0.84, 1.3, 1.35)),
2710 ]);
2711 assert_eq!(
2712 nested.side_rail(RailSide::Left),
2713 Err(vec![id("p"), id("q")])
2714 );
2715 assert_eq!(nested.side_rail(RailSide::Right), Ok(vec![]));
2716 let blurred = RailMeasurement::try_new(
2719 (around(0.5, 0.2), around(0.6, 0.2)),
2720 (point(1.25), point(1.3)),
2721 MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2722 MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2723 )
2724 .unwrap();
2725 assert_eq!(measured.gap(&left[0].1, &blurred), None);
2726 }
2727
2728 #[test]
2729 fn a_turning_flights_rails_are_measured_part_by_part() {
2730 let y = MetricDirection::try_new([0.0, 1.0, 0.0]).unwrap();
2734 let parts = vec![
2735 StretchPart::try_new(x(), (point(0.0), point(1.0))).unwrap(),
2736 StretchPart::try_new(y, (point(-1.84), point(-0.84))).unwrap(),
2737 ];
2738 let request = HandrailRequest::try_new(
2739 id("a"),
2740 WalkingStretch::Flight,
2741 [id("p"), id("q"), id("r")],
2742 (0.2, 1.5),
2743 0.3,
2744 )
2745 .unwrap();
2746 let measure = |rails: Vec<(ObjectId, RailMeasurement)>| {
2747 HandrailEvidence::try_in_parts(
2748 request.clone(),
2749 parts.clone(),
2750 (point(0.0), point(1.56)),
2751 rails,
2752 evidence(false),
2753 )
2754 };
2755 let measured = measure(vec![
2758 (id("p"), piece(-0.3, 1.89, -0.1, -0.05)),
2759 (id("q"), piece(-0.1, 1.86, -1.89, -1.84).in_part(1)),
2760 (id("r"), piece(1.0, 1.86, -0.84, -0.79).in_part(1)),
2761 ])
2762 .unwrap();
2763 assert_eq!(measured.parts().len(), 2);
2764 let (p, q, r) = (
2765 &measured.rails()[0].1,
2766 &measured.rails()[1].1,
2767 &measured.rails()[2].1,
2768 );
2769 assert_eq!(measured.side(p), Some(RailSide::Right));
2770 assert_eq!(measured.side(q), Some(RailSide::Right));
2771 assert_eq!(measured.side(r), Some(RailSide::Left));
2772 let outer = measured.side_rail(RailSide::Right).unwrap();
2773 let names: Vec<&ObjectId> = outer.iter().map(|(rail, _)| rail).collect();
2774 assert_eq!(names, [&id("p"), &id("q")]);
2775 let gap = measured.gap(p, q).unwrap();
2777 assert!(gap.lower() == 0.0 && gap.upper() < 1e-12, "{gap:?}");
2778 assert!(contains(measured.bottom_extension(p).unwrap(), 0.3));
2781 assert_eq!(measured.top_extension(p), None);
2782 assert!(contains(measured.top_extension(q).unwrap(), 0.3));
2783 assert_eq!(measured.bottom_extension(q), None);
2784 assert_eq!(
2786 measure(vec![(id("p"), piece(0.0, 1.0, -0.1, -0.05).in_part(2))]),
2787 Err(WalkingSurfaceError::InvalidMeasurement)
2788 );
2789 assert_eq!(
2790 HandrailEvidence::try_in_parts(
2791 request.clone(),
2792 vec![],
2793 (point(0.0), point(1.56)),
2794 vec![],
2795 evidence(false)
2796 ),
2797 Err(WalkingSurfaceError::InvalidMeasurement)
2798 );
2799 let sloped = MetricDirection::try_new([1.0, 0.0, 1.0]).unwrap();
2800 assert!(StretchPart::try_new(sloped, (point(0.0), point(1.0))).is_err());
2801 }
2802
2803 fn around(value: f64, margin: f64) -> ElevationInterval {
2804 ElevationInterval::try_new(value - margin, value + margin).unwrap()
2805 }
2806
2807 #[test]
2808 fn widths_come_from_the_sides_and_the_narrowest_tread_governs() {
2809 let sided = |z: f64, front: f64, right: f64| {
2810 tread(z, front, front + 0.3)
2811 .with_sides(point(0.0), point(right))
2812 .unwrap()
2813 };
2814 let treads = vec![sided(0.2, 0.0, 1.2), sided(0.4, 0.3, 1.1)];
2815 let flight = TreadFlight::try_new(
2816 request("a"),
2817 WalkingLine::Straight(x()),
2818 point(0.0),
2819 point(0.4),
2820 treads,
2821 evidence(true),
2822 )
2823 .unwrap();
2824 assert!(contains(flight.width().unwrap(), 1.1));
2825 let treads = vec![sided(0.2, 0.0, 1.2), tread(0.4, 0.3, 0.6)];
2827 let flight = TreadFlight::try_new(
2828 request("a"),
2829 WalkingLine::Straight(x()),
2830 point(0.0),
2831 point(0.4),
2832 treads,
2833 evidence(true),
2834 )
2835 .unwrap();
2836 assert_eq!(flight.width(), None);
2837 assert_eq!(
2839 tread(0.2, 0.0, 0.3).with_sides(point(1.0), point(0.0)),
2840 Err(WalkingSurfaceError::InvalidMeasurement)
2841 );
2842 let widened = tread(0.2, 0.0, 0.3)
2843 .with_sides(point(0.0), ElevationInterval::try_new(1.0, 1.1).unwrap())
2844 .unwrap();
2845 assert!(!widened.is_exact());
2846 assert_eq!(
2847 TreadFlight::try_new(
2848 request("a"),
2849 WalkingLine::Straight(x()),
2850 point(0.0),
2851 point(0.2),
2852 vec![widened],
2853 evidence(true)
2854 ),
2855 Err(WalkingSurfaceError::InexactEvidence)
2856 );
2857 let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(1.0), point(7.0))
2858 .unwrap()
2859 .with_sides(point(-0.75), point(0.75))
2860 .unwrap();
2861 assert!(contains(run.width().unwrap(), 1.5) && run.is_exact());
2862 let [ax, ay, _] = across(x()).components();
2863 assert!(ax.abs() < f64::EPSILON && (ay - 1.0).abs() < f64::EPSILON);
2864 }
2865
2866 fn landing_evidence(
2867 request: &LandingRequest,
2868 landing: Option<Landing>,
2869 exact: bool,
2870 ) -> Result<LandingEvidence, WalkingSurfaceError> {
2871 LandingEvidence::try_new(
2872 request.clone(),
2873 x(),
2874 point(1.0),
2875 landing,
2876 Evidence {
2877 source: source(),
2878 locator: "landing:a".into(),
2879 exact,
2880 },
2881 )
2882 }
2883
2884 #[test]
2885 fn a_landing_is_carried_by_a_requested_object_beyond_the_edge() {
2886 let request = LandingRequest::new(id("a"), WalkingEnd::FlightTop, [id("c"), id("a")]);
2887 assert_eq!(request.candidates(), &[id("c")]);
2888 let extent = LandingExtent::try_new(point(2.5), point(-0.1), point(1.4)).unwrap();
2889 let found =
2890 landing_evidence(&request, Some(Landing::new(id("c"), Some(extent))), true).unwrap();
2891 assert!(contains(found.depth().unwrap(), 1.5));
2892 assert!(contains(found.width().unwrap(), 1.5));
2893 assert!(
2895 landing_evidence(&request, Some(Landing::new(id("a"), Some(extent))), true).is_ok()
2896 );
2897 assert_eq!(
2900 landing_evidence(&request, Some(Landing::new(id("d"), None)), true),
2901 Err(WalkingSurfaceError::InvalidMeasurement)
2902 );
2903 let short = LandingExtent::try_new(point(0.5), point(0.0), point(1.0)).unwrap();
2904 assert_eq!(
2905 landing_evidence(&request, Some(Landing::new(id("c"), Some(short))), true),
2906 Err(WalkingSurfaceError::InvalidMeasurement)
2907 );
2908 assert_eq!(
2909 landing_evidence(&request, None, false),
2910 Err(WalkingSurfaceError::InexactEvidence)
2911 );
2912 assert_eq!(
2913 LandingExtent::try_new(point(2.0), point(1.0), point(0.0)),
2914 Err(WalkingSurfaceError::InvalidMeasurement)
2915 );
2916 let unmeasured =
2918 landing_evidence(&request, Some(Landing::new(id("c"), None)), true).unwrap();
2919 assert_eq!(unmeasured.depth(), None);
2920 assert_eq!(unmeasured.width(), None);
2921 }
2922
2923 #[test]
2924 fn the_clearance_below_names_requested_spaces_only() {
2925 let request = ClearanceBelowRequest::new(id("a"), [id("s"), id("a"), id("s")]);
2926 assert_eq!(request.spaces(), &[id("s")]);
2927 let clearance = MeasuredInterval::try_new(1.5, 1.5 + 1e-9).ok();
2928 assert!(
2929 ClearanceBelow::try_new(request.clone(), clearance, vec![id("s")], evidence(false))
2930 .is_ok()
2931 );
2932 assert_eq!(
2933 ClearanceBelow::try_new(request.clone(), clearance, vec![id("t")], evidence(false)),
2934 Err(WalkingSurfaceError::InvalidMeasurement)
2935 );
2936 assert_eq!(
2937 ClearanceBelow::try_new(request.clone(), clearance, vec![id("s")], evidence(true)),
2938 Err(WalkingSurfaceError::InexactEvidence)
2939 );
2940 assert!(ClearanceBelow::try_new(request, None, vec![], evidence(false)).is_ok());
2941 }
2942
2943 #[test]
2944 fn winder_angles_and_widths_come_from_nosings_and_sides() {
2945 let nosing = |degrees: f64| {
2948 let (sin, cos) = degrees.to_radians().sin_cos();
2949 PlanSegment::try_new([0.0, 0.0], [cos, sin], 0.0).unwrap()
2950 };
2951 let treads: Vec<Tread> = [0.0, 30.0, 60.0, 90.0]
2952 .iter()
2953 .enumerate()
2954 .map(|(step, degrees)| {
2955 #[allow(clippy::cast_precision_loss)]
2956 let step = step as f64;
2957 tread(0.18 * (step + 1.0), 0.3 * step, 0.3 * step + 0.3)
2958 .with_nosing(nosing(*degrees))
2959 .with_sides(point(-0.4), point(0.5))
2960 .unwrap()
2961 .with_riser_below(RiserClosure::Closed)
2962 })
2963 .collect();
2964 let line = WalkingLine::Turning(vec![[0.5, 0.1], [0.4, 0.3], [0.3, 0.4], [0.1, 0.5]]);
2965 let flight = TreadFlight::try_new(
2966 request("a"),
2967 line.clone(),
2968 point(0.0),
2969 point(0.72),
2970 treads,
2971 evidence(false),
2972 )
2973 .unwrap_err();
2974 assert_eq!(flight, WalkingSurfaceError::InexactEvidence);
2976 let treads: Vec<Tread> = [0.0, 30.0, 60.0, 90.0]
2977 .iter()
2978 .enumerate()
2979 .map(|(step, degrees)| {
2980 #[allow(clippy::cast_precision_loss)]
2981 let step = step as f64;
2982 let tread = tread(0.18 * (step + 1.0), 0.3 * step, 0.3 * step + 0.3)
2983 .with_nosing(nosing(*degrees));
2984 if step == 0.0 {
2986 tread.with_sides(point(-0.4), point(0.5)).unwrap()
2987 } else {
2988 tread
2989 }
2990 })
2991 .collect();
2992 let flight = TreadFlight::try_new(
2993 request("a"),
2994 line,
2995 point(0.0),
2996 point(0.72),
2997 treads,
2998 evidence(true),
2999 )
3000 .unwrap();
3001 assert!(flight.walking_line().is_turning());
3002 let angles = flight.winder_angles();
3003 assert_eq!(angles.len(), 3);
3004 for angle in angles {
3005 let angle = angle.unwrap();
3006 assert!(contains(angle, 30.0_f64.to_radians()), "{angle:?}");
3007 assert!(angle.upper() - angle.lower() < 1e-12);
3008 }
3009 let width = flight.treads()[0].width().unwrap();
3010 assert!(contains(width, 0.9));
3011 assert_eq!(flight.treads()[1].width(), None);
3013 assert_eq!(flight.width(), None);
3014 assert_eq!(flight.treads()[0].riser_below(), RiserClosure::NotMeasured);
3015 }
3016
3017 #[test]
3018 fn an_uncertain_nosing_widens_its_angle_and_a_short_one_has_none() {
3019 let a = PlanSegment::try_new([0.0, 0.0], [1.0, 0.0], 0.001).unwrap();
3020 let b = PlanSegment::try_new([0.0, 0.0], [0.0, 1.0], 0.001).unwrap();
3021 let square = a.angle_to(&b).unwrap();
3022 assert!(square.upper() <= std::f64::consts::FRAC_PI_2);
3023 assert!(square.lower() < std::f64::consts::FRAC_PI_2 - 0.0039);
3024 let parallel = a.angle_to(&a).unwrap();
3025 assert!(parallel.lower() == 0.0 && parallel.upper() > 0.0039);
3026 let short = PlanSegment::try_new([0.0, 0.0], [0.001, 0.0], 0.001).unwrap();
3027 assert_eq!(short.angle_to(&a), None);
3028 assert_eq!(
3029 PlanSegment::try_new([0.0, 0.0], [0.0, 0.0], 0.0),
3030 Err(WalkingSurfaceError::InvalidMeasurement)
3031 );
3032 assert_eq!(
3033 PlanSegment::try_new([0.0, 0.0], [1.0, 0.0], -1.0),
3034 Err(WalkingSurfaceError::InvalidMeasurement)
3035 );
3036 }
3037
3038 #[test]
3039 fn clear_widths_name_requested_obstacles_and_a_band_above_the_pitch_line() {
3040 let request = ClearWidthRequest::try_new(
3041 id("f"),
3042 WalkingStretch::Flight,
3043 [id("rail"), id("f"), id("rail")],
3044 (0.5, 1.5),
3045 )
3046 .unwrap();
3047 assert_eq!(request.obstacles(), [id("rail")]);
3048 assert_eq!(request.band(), (0.5, 1.5));
3049 for band in [(1.5, 0.5), (-0.1, 1.0), (0.5, f64::NAN)] {
3050 assert!(ClearWidthRequest::try_new(id("f"), WalkingStretch::Flight, [], band).is_err());
3051 }
3052 let width = MeasuredInterval::try_new(0.99, 1.01).unwrap();
3053 let measured =
3054 ClearWidthEvidence::try_new(request.clone(), width, vec![id("rail")], evidence(false))
3055 .unwrap();
3056 assert_eq!(measured.governing(), [id("rail")]);
3057 assert_eq!(
3058 ClearWidthEvidence::try_new(request.clone(), width, vec![id("wall")], evidence(false)),
3059 Err(WalkingSurfaceError::InvalidMeasurement)
3060 );
3061 assert_eq!(
3062 ClearWidthEvidence::try_new(request, width, vec![], evidence(true)),
3063 Err(WalkingSurfaceError::InexactEvidence)
3064 );
3065 }
3066
3067 #[test]
3068 fn walking_lines_and_offsets_are_checked() {
3069 assert!(TreadFlightRequest::from_inner_side(id("a"), 0.0).is_err());
3070 assert!(TreadFlightRequest::from_inner_side(id("a"), f64::NAN).is_err());
3071 let request = TreadFlightRequest::from_inner_side(id("a"), 0.4).unwrap();
3072 assert_eq!(
3073 request.walking_line(),
3074 WalkingLinePlacement::FromInnerSide(0.4)
3075 );
3076 let treads = || vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
3077 for line in [
3078 WalkingLine::Turning(vec![[0.0, 0.0]]),
3079 WalkingLine::Turning(vec![[0.0, 0.0], [0.0, 0.0]]),
3080 WalkingLine::Turning(vec![[0.0, 0.0], [f64::INFINITY, 0.0]]),
3081 ] {
3082 assert_eq!(
3083 TreadFlight::try_new(
3084 request.clone(),
3085 line,
3086 point(0.0),
3087 point(0.4),
3088 treads(),
3089 evidence(true)
3090 ),
3091 Err(WalkingSurfaceError::InvalidMeasurement)
3092 );
3093 }
3094 assert!(
3095 tread(0.2, 0.0, 0.3)
3096 .with_sides(point(1.0), point(0.0))
3097 .is_err()
3098 );
3099 }
3100}