1use std::sync::Arc;
8
9use crate::services::reviewable_exact_evidence;
10use crate::walkability::VerticalConnector;
11use axioval_ir::{Evidence, ObjectId};
12use thiserror::Error;
13
14#[derive(Clone, Debug, Error, PartialEq, Eq)]
16pub enum MetricRoutingError {
17 #[error("metric point coordinates must be finite")]
19 InvalidCoordinate,
20 #[error("metric length interval is invalid")]
22 InvalidLengthInterval,
23 #[error("mobility profile contains an invalid dimension")]
25 InvalidMobilityProfile,
26 #[error("metric route evidence is empty")]
28 EmptyRouteEvidence,
29 #[error("metric route provenance is not exact and reviewable")]
31 InexactRouteEvidence,
32 #[error("metric evidence is incomplete")]
34 IncompleteMetricEvidence,
35 #[error("metric routing backend returned mismatched endpoints")]
37 ResponseEndpointMismatch,
38 #[error("metric geometry is unavailable for `{0}`")]
40 MissingGeometry(Box<ObjectId>),
41 #[error("metric routing query unavailable: {0}")]
43 Unavailable(String),
44 #[error("a metric routing query needs at least one target")]
46 NoTargets,
47 #[error("metric routing tolerance must be finite and non-negative")]
49 InvalidTolerance,
50 #[error("metric routing backend answered inconsistently with the request")]
53 InconsistentResponse,
54 #[error("a climb's vertical factor must be finite and non-negative")]
56 InvalidClimb,
57 #[error("a vertical connector is given twice with different kinds")]
59 ConflictingConnector,
60 #[error("a travel cost factor must be finite and at least one")]
62 InvalidCostFactor,
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum ThresholdVerdict {
68 Satisfied,
70 Violated,
72 Indeterminate,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct LengthInterval {
79 lower_metres: f64,
80 upper_metres: f64,
81}
82
83impl LengthInterval {
84 pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, MetricRoutingError> {
86 if !valid_non_negative(lower_metres)
87 || !valid_non_negative(upper_metres)
88 || lower_metres > upper_metres
89 {
90 return Err(MetricRoutingError::InvalidLengthInterval);
91 }
92 Ok(Self {
93 lower_metres,
94 upper_metres,
95 })
96 }
97
98 pub fn exact(metres: f64) -> Result<Self, MetricRoutingError> {
100 Self::try_new(metres, metres)
101 }
102
103 pub fn lower_metres(&self) -> f64 {
105 self.lower_metres
106 }
107
108 pub fn upper_metres(&self) -> f64 {
110 self.upper_metres
111 }
112
113 #[allow(clippy::float_cmp)]
115 pub fn is_exact(&self) -> bool {
116 self.lower_metres == self.upper_metres
119 }
120
121 pub fn compare_maximum(
123 &self,
124 maximum_metres: f64,
125 ) -> Result<ThresholdVerdict, MetricRoutingError> {
126 if !valid_non_negative(maximum_metres) {
127 return Err(MetricRoutingError::InvalidLengthInterval);
128 }
129 if self.upper_metres <= maximum_metres {
130 Ok(ThresholdVerdict::Satisfied)
131 } else if self.lower_metres > maximum_metres {
132 Ok(ThresholdVerdict::Violated)
133 } else {
134 Ok(ThresholdVerdict::Indeterminate)
135 }
136 }
137}
138
139#[derive(Clone, Debug, PartialEq)]
141pub struct MetricPoint {
142 subject: ObjectId,
143 coordinates_metres: [f64; 3],
144}
145
146impl MetricPoint {
147 pub fn try_new(
149 subject: ObjectId,
150 coordinates_metres: [f64; 3],
151 ) -> Result<Self, MetricRoutingError> {
152 if !coordinates_metres.iter().all(|value| value.is_finite()) {
153 return Err(MetricRoutingError::InvalidCoordinate);
154 }
155 Ok(Self {
156 subject,
157 coordinates_metres,
158 })
159 }
160
161 pub fn subject(&self) -> &ObjectId {
163 &self.subject
164 }
165
166 pub fn coordinates_metres(&self) -> [f64; 3] {
168 self.coordinates_metres
169 }
170}
171
172#[derive(Clone, Copy, Debug, PartialEq)]
174pub struct MobilityProfile {
175 radius_metres: f64,
176 height_metres: f64,
177 maximum_step_metres: f64,
178 maximum_slope: f64,
179}
180
181impl MobilityProfile {
182 pub fn try_new(
184 radius_metres: f64,
185 height_metres: f64,
186 maximum_step_metres: f64,
187 maximum_slope: f64,
188 ) -> Result<Self, MetricRoutingError> {
189 if ![
190 radius_metres,
191 height_metres,
192 maximum_step_metres,
193 maximum_slope,
194 ]
195 .into_iter()
196 .all(valid_non_negative)
197 {
198 return Err(MetricRoutingError::InvalidMobilityProfile);
199 }
200 Ok(Self {
201 radius_metres,
202 height_metres,
203 maximum_step_metres,
204 maximum_slope,
205 })
206 }
207
208 pub fn radius_metres(&self) -> f64 {
210 self.radius_metres
211 }
212
213 pub fn height_metres(&self) -> f64 {
215 self.height_metres
216 }
217
218 pub fn maximum_step_metres(&self) -> f64 {
220 self.maximum_step_metres
221 }
222
223 pub fn maximum_slope(&self) -> f64 {
225 self.maximum_slope
226 }
227}
228
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum StairLength {
232 Slope,
235 HorizontalPlusVertical,
238}
239
240impl StairLength {
241 #[must_use]
244 pub fn as_str(self) -> &'static str {
245 match self {
246 Self::Slope => "slope",
247 Self::HorizontalPlusVertical => "horizontal-plus-vertical",
248 }
249 }
250}
251
252#[derive(Clone, Copy, Debug, PartialEq)]
254pub struct ClimbLength {
255 measure: StairLength,
256 vertical_factor: f64,
257}
258
259impl ClimbLength {
260 pub fn try_new(measure: StairLength, vertical_factor: f64) -> Result<Self, MetricRoutingError> {
266 if !valid_non_negative(vertical_factor) {
267 return Err(MetricRoutingError::InvalidClimb);
268 }
269 Ok(Self {
270 measure,
271 vertical_factor,
272 })
273 }
274
275 #[must_use]
277 pub fn slope() -> Self {
278 Self {
279 measure: StairLength::Slope,
280 vertical_factor: 1.0,
281 }
282 }
283
284 #[must_use]
286 pub fn measure(&self) -> StairLength {
287 self.measure
288 }
289
290 #[must_use]
292 pub fn vertical_factor(&self) -> f64 {
293 self.vertical_factor
294 }
295
296 #[must_use]
300 pub fn length(&self, horizontal: LengthInterval, rise: LengthInterval) -> LengthInterval {
301 let at = |h: f64, v: f64| match self.measure {
302 StairLength::Slope => h.hypot(self.vertical_factor * v),
303 StairLength::HorizontalPlusVertical => self.vertical_factor.mul_add(v, h),
304 };
305 let lower = at(horizontal.lower_metres(), rise.lower_metres());
306 let upper = at(horizontal.upper_metres(), rise.upper_metres());
307 LengthInterval {
308 lower_metres: (lower * (1.0 - CLIMB_ROUNDING)).max(0.0),
309 upper_metres: upper * (1.0 + CLIMB_ROUNDING),
310 }
311 }
312}
313
314const CLIMB_ROUNDING: f64 = 4.0 * f64::EPSILON;
316
317#[derive(Clone, Debug, PartialEq)]
327pub struct ConnectorRouting {
328 connectors: Vec<VerticalConnector>,
329 climb: ClimbLength,
330}
331
332impl ConnectorRouting {
333 pub fn try_new(
340 mut connectors: Vec<VerticalConnector>,
341 climb: ClimbLength,
342 ) -> Result<Self, MetricRoutingError> {
343 connectors.sort();
344 connectors.dedup();
345 if connectors
346 .windows(2)
347 .any(|pair| pair[0].object() == pair[1].object())
348 {
349 return Err(MetricRoutingError::ConflictingConnector);
350 }
351 Ok(Self { connectors, climb })
352 }
353
354 #[must_use]
356 pub fn connectors(&self) -> &[VerticalConnector] {
357 &self.connectors
358 }
359
360 #[must_use]
362 pub fn climb(&self) -> ClimbLength {
363 self.climb
364 }
365}
366
367#[derive(Clone, Debug, PartialEq)]
383pub struct TravelCost {
384 object: ObjectId,
385 factor: f64,
386}
387
388impl TravelCost {
389 pub fn try_new(object: ObjectId, factor: f64) -> Result<Self, MetricRoutingError> {
395 if !(factor.is_finite() && factor >= 1.0) {
396 return Err(MetricRoutingError::InvalidCostFactor);
397 }
398 Ok(Self { object, factor })
399 }
400
401 #[must_use]
403 pub fn object(&self) -> &ObjectId {
404 &self.object
405 }
406
407 #[must_use]
409 pub fn factor(&self) -> f64 {
410 self.factor
411 }
412}
413
414fn settled(mut costs: Vec<TravelCost>) -> Vec<TravelCost> {
417 costs.retain(|cost| cost.factor > 1.0);
418 costs.sort_by(|a, b| a.object.cmp(&b.object).then(b.factor.total_cmp(&a.factor)));
419 costs.dedup_by(|later, earlier| later.object == earlier.object);
420 costs
421}
422
423#[derive(Clone, Debug, PartialEq)]
425pub struct MetricRouteRequest {
426 origin: MetricPoint,
427 destination: MetricPoint,
428 profile: MobilityProfile,
429 connectors: Option<ConnectorRouting>,
430}
431
432impl MetricRouteRequest {
433 pub fn new(origin: MetricPoint, destination: MetricPoint, profile: MobilityProfile) -> Self {
435 Self {
436 origin,
437 destination,
438 profile,
439 connectors: None,
440 }
441 }
442
443 #[must_use]
447 pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
448 self.connectors = Some(connectors);
449 self
450 }
451
452 pub fn connectors(&self) -> Option<&ConnectorRouting> {
455 self.connectors.as_ref()
456 }
457
458 pub fn origin(&self) -> &MetricPoint {
460 &self.origin
461 }
462
463 pub fn destination(&self) -> &MetricPoint {
465 &self.destination
466 }
467
468 pub fn profile(&self) -> MobilityProfile {
470 self.profile
471 }
472}
473
474#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct CompleteMetricEvidence(Evidence);
477
478impl CompleteMetricEvidence {
479 pub fn try_new(evidence: Evidence) -> Result<Self, MetricRoutingError> {
481 if !reviewable_exact_evidence(&evidence) {
482 return Err(MetricRoutingError::IncompleteMetricEvidence);
483 }
484 Ok(Self(evidence))
485 }
486
487 pub fn evidence(&self) -> &Evidence {
489 &self.0
490 }
491}
492
493#[derive(Clone, Debug, PartialEq)]
495pub struct BlockedMetricRouteEvidence {
496 request: MetricRouteRequest,
497 completeness: CompleteMetricEvidence,
498}
499
500impl BlockedMetricRouteEvidence {
501 pub fn new(request: MetricRouteRequest, completeness: CompleteMetricEvidence) -> Self {
503 Self {
504 request,
505 completeness,
506 }
507 }
508
509 pub fn request(&self) -> &MetricRouteRequest {
511 &self.request
512 }
513
514 pub fn completeness(&self) -> &CompleteMetricEvidence {
516 &self.completeness
517 }
518}
519
520#[derive(Clone, Debug, PartialEq)]
522pub struct MetricRouteEvidence {
523 shortest_distance: LengthInterval,
524 waypoints: Vec<MetricPoint>,
525 traversed_objects: Vec<ObjectId>,
526 evidence: Evidence,
527}
528
529impl MetricRouteEvidence {
530 pub fn try_new(
532 shortest_distance: LengthInterval,
533 waypoints: Vec<MetricPoint>,
534 traversed_objects: Vec<ObjectId>,
535 evidence: Evidence,
536 ) -> Result<Self, MetricRoutingError> {
537 if waypoints.is_empty() || traversed_objects.is_empty() {
538 return Err(MetricRoutingError::EmptyRouteEvidence);
539 }
540 if !reviewable_exact_evidence(&evidence) {
541 return Err(MetricRoutingError::InexactRouteEvidence);
542 }
543 Ok(Self {
544 shortest_distance,
545 waypoints,
546 traversed_objects,
547 evidence,
548 })
549 }
550
551 pub fn shortest_distance(&self) -> &LengthInterval {
553 &self.shortest_distance
554 }
555
556 pub fn waypoints(&self) -> &[MetricPoint] {
558 &self.waypoints
559 }
560
561 pub fn traversed_objects(&self) -> &[ObjectId] {
563 &self.traversed_objects
564 }
565
566 pub fn evidence(&self) -> &Evidence {
568 &self.evidence
569 }
570}
571
572#[derive(Clone, Debug, PartialEq)]
574pub enum MetricRouteOutcome {
575 Reachable(MetricRouteEvidence),
577 Blocked(BlockedMetricRouteEvidence),
579}
580
581#[derive(Clone, Debug, PartialEq)]
596pub struct NearestTargetRequest {
597 origin: MetricPoint,
598 targets: Vec<MetricPoint>,
599 profile: MobilityProfile,
600 avoided: Vec<ObjectId>,
601 connectors: Option<ConnectorRouting>,
602 costs: Vec<TravelCost>,
603}
604
605impl NearestTargetRequest {
606 pub fn try_new(
608 origin: MetricPoint,
609 targets: Vec<MetricPoint>,
610 profile: MobilityProfile,
611 ) -> Result<Self, MetricRoutingError> {
612 if targets.is_empty() {
613 return Err(MetricRoutingError::NoTargets);
614 }
615 Ok(Self {
616 origin,
617 targets,
618 profile,
619 avoided: Vec::new(),
620 connectors: None,
621 costs: Vec::new(),
622 })
623 }
624
625 #[must_use]
629 pub fn with_costs(mut self, costs: Vec<TravelCost>) -> Self {
630 self.costs = settled(costs);
631 self
632 }
633
634 pub fn costs(&self) -> &[TravelCost] {
637 &self.costs
638 }
639
640 #[must_use]
643 pub fn with_avoided(mut self, mut avoided: Vec<ObjectId>) -> Self {
644 avoided.sort();
645 avoided.dedup();
646 self.avoided = avoided;
647 self
648 }
649
650 #[must_use]
653 pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
654 self.connectors = Some(connectors);
655 self
656 }
657
658 pub fn connectors(&self) -> Option<&ConnectorRouting> {
661 self.connectors.as_ref()
662 }
663
664 pub fn origin(&self) -> &MetricPoint {
666 &self.origin
667 }
668
669 pub fn targets(&self) -> &[MetricPoint] {
671 &self.targets
672 }
673
674 pub fn profile(&self) -> MobilityProfile {
676 self.profile
677 }
678
679 pub fn avoided(&self) -> &[ObjectId] {
681 &self.avoided
682 }
683}
684
685#[derive(Clone, Debug, PartialEq)]
688pub struct NearestTargetEvidence {
689 target: usize,
690 shortest_distance: LengthInterval,
691 waypoints: Vec<MetricPoint>,
692 evidence: Evidence,
693}
694
695impl NearestTargetEvidence {
696 pub fn try_new(
700 target: usize,
701 shortest_distance: LengthInterval,
702 waypoints: Vec<MetricPoint>,
703 evidence: Evidence,
704 ) -> Result<Self, MetricRoutingError> {
705 if waypoints.is_empty() {
706 return Err(MetricRoutingError::EmptyRouteEvidence);
707 }
708 if !reviewable_exact_evidence(&evidence) {
709 return Err(MetricRoutingError::InexactRouteEvidence);
710 }
711 Ok(Self {
712 target,
713 shortest_distance,
714 waypoints,
715 evidence,
716 })
717 }
718
719 pub fn target(&self) -> usize {
721 self.target
722 }
723
724 pub fn shortest_distance(&self) -> &LengthInterval {
726 &self.shortest_distance
727 }
728
729 pub fn waypoints(&self) -> &[MetricPoint] {
731 &self.waypoints
732 }
733
734 pub fn evidence(&self) -> &Evidence {
736 &self.evidence
737 }
738}
739
740#[derive(Clone, Debug, PartialEq)]
742pub struct UnreachableTargetsEvidence {
743 request: NearestTargetRequest,
744 completeness: CompleteMetricEvidence,
745}
746
747impl UnreachableTargetsEvidence {
748 pub fn new(request: NearestTargetRequest, completeness: CompleteMetricEvidence) -> Self {
750 Self {
751 request,
752 completeness,
753 }
754 }
755
756 pub fn request(&self) -> &NearestTargetRequest {
758 &self.request
759 }
760
761 pub fn completeness(&self) -> &CompleteMetricEvidence {
763 &self.completeness
764 }
765}
766
767#[derive(Clone, Debug, PartialEq)]
769pub enum NearestTargetOutcome {
770 Reached(NearestTargetEvidence),
772 Unreachable(UnreachableTargetsEvidence),
774}
775
776#[derive(Clone, Debug, PartialEq)]
787pub struct FarthestPointRequest {
788 region: ObjectId,
789 targets: Vec<MetricPoint>,
790 profile: MobilityProfile,
791 tolerance_metres: f64,
792 connectors: Option<ConnectorRouting>,
793 costs: Vec<TravelCost>,
794}
795
796impl FarthestPointRequest {
797 pub fn try_new(
801 region: ObjectId,
802 targets: Vec<MetricPoint>,
803 profile: MobilityProfile,
804 tolerance_metres: f64,
805 ) -> Result<Self, MetricRoutingError> {
806 if targets.is_empty() {
807 return Err(MetricRoutingError::NoTargets);
808 }
809 if !valid_non_negative(tolerance_metres) {
810 return Err(MetricRoutingError::InvalidTolerance);
811 }
812 Ok(Self {
813 region,
814 targets,
815 profile,
816 tolerance_metres,
817 connectors: None,
818 costs: Vec::new(),
819 })
820 }
821
822 #[must_use]
826 pub fn with_costs(mut self, costs: Vec<TravelCost>) -> Self {
827 self.costs = settled(costs);
828 self
829 }
830
831 pub fn costs(&self) -> &[TravelCost] {
834 &self.costs
835 }
836
837 #[must_use]
840 pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
841 self.connectors = Some(connectors);
842 self
843 }
844
845 pub fn connectors(&self) -> Option<&ConnectorRouting> {
848 self.connectors.as_ref()
849 }
850
851 pub fn region(&self) -> &ObjectId {
853 &self.region
854 }
855
856 pub fn targets(&self) -> &[MetricPoint] {
858 &self.targets
859 }
860
861 pub fn profile(&self) -> MobilityProfile {
863 self.profile
864 }
865
866 pub fn tolerance_metres(&self) -> f64 {
868 self.tolerance_metres
869 }
870}
871
872#[derive(Clone, Debug, PartialEq)]
874pub struct FarthestPointEvidence {
875 distance: LengthInterval,
876 witness: MetricPoint,
877 converged: bool,
878 evidence: Evidence,
879}
880
881impl FarthestPointEvidence {
882 pub fn try_new(
889 distance: LengthInterval,
890 witness: MetricPoint,
891 converged: bool,
892 evidence: Evidence,
893 ) -> Result<Self, MetricRoutingError> {
894 if !reviewable_exact_evidence(&evidence) {
895 return Err(MetricRoutingError::InexactRouteEvidence);
896 }
897 Ok(Self {
898 distance,
899 witness,
900 converged,
901 evidence,
902 })
903 }
904
905 pub fn distance(&self) -> &LengthInterval {
907 &self.distance
908 }
909
910 pub fn witness(&self) -> &MetricPoint {
912 &self.witness
913 }
914
915 pub fn converged(&self) -> bool {
917 self.converged
918 }
919
920 pub fn evidence(&self) -> &Evidence {
922 &self.evidence
923 }
924}
925
926#[derive(Clone, Debug, PartialEq)]
929pub struct UnreachableRegionEvidence {
930 request: FarthestPointRequest,
931 witness: MetricPoint,
932 completeness: CompleteMetricEvidence,
933}
934
935impl UnreachableRegionEvidence {
936 pub fn new(
939 request: FarthestPointRequest,
940 witness: MetricPoint,
941 completeness: CompleteMetricEvidence,
942 ) -> Self {
943 Self {
944 request,
945 witness,
946 completeness,
947 }
948 }
949
950 pub fn request(&self) -> &FarthestPointRequest {
952 &self.request
953 }
954
955 pub fn witness(&self) -> &MetricPoint {
957 &self.witness
958 }
959
960 pub fn completeness(&self) -> &CompleteMetricEvidence {
962 &self.completeness
963 }
964}
965
966#[derive(Clone, Debug, PartialEq)]
968pub enum FarthestPointOutcome {
969 Bounded(FarthestPointEvidence),
972 Unreachable(UnreachableRegionEvidence),
974}
975
976#[derive(Clone, Debug, PartialEq)]
983pub struct PathTraceRequest {
984 waypoints: Vec<MetricPoint>,
985 objects: Vec<ObjectId>,
986}
987
988impl PathTraceRequest {
989 pub fn try_new(
992 waypoints: Vec<MetricPoint>,
993 mut objects: Vec<ObjectId>,
994 ) -> Result<Self, MetricRoutingError> {
995 if waypoints.is_empty() {
996 return Err(MetricRoutingError::EmptyRouteEvidence);
997 }
998 objects.sort();
999 objects.dedup();
1000 Ok(Self { waypoints, objects })
1001 }
1002
1003 pub fn waypoints(&self) -> &[MetricPoint] {
1005 &self.waypoints
1006 }
1007
1008 pub fn objects(&self) -> &[ObjectId] {
1010 &self.objects
1011 }
1012
1013 pub fn plan_length_metres(&self) -> f64 {
1015 self.waypoints
1016 .windows(2)
1017 .map(|pair| {
1018 let ([ax, ay, _], [bx, by, _]) =
1019 (pair[0].coordinates_metres(), pair[1].coordinates_metres());
1020 (bx - ax).hypot(by - ay)
1021 })
1022 .sum()
1023 }
1024}
1025
1026#[derive(Clone, Debug, PartialEq)]
1033pub struct PathTrace {
1034 lengths: Vec<Result<LengthInterval, String>>,
1035 evidence: Evidence,
1036}
1037
1038impl PathTrace {
1039 pub fn try_new(
1041 lengths: Vec<Result<LengthInterval, String>>,
1042 evidence: Evidence,
1043 ) -> Result<Self, MetricRoutingError> {
1044 if !reviewable_exact_evidence(&evidence) {
1045 return Err(MetricRoutingError::InexactRouteEvidence);
1046 }
1047 Ok(Self { lengths, evidence })
1048 }
1049
1050 pub fn lengths(&self) -> &[Result<LengthInterval, String>] {
1052 &self.lengths
1053 }
1054
1055 pub fn evidence(&self) -> &Evidence {
1057 &self.evidence
1058 }
1059}
1060
1061#[derive(Clone, Debug, PartialEq)]
1069pub struct ForcedWalkRequest {
1070 origin: MetricPoint,
1071 targets: Vec<MetricPoint>,
1072 through: ObjectId,
1073 profile: MobilityProfile,
1074 tolerance_metres: f64,
1075 avoided: Vec<ObjectId>,
1076 connectors: Option<ConnectorRouting>,
1077}
1078
1079impl ForcedWalkRequest {
1080 pub fn try_new(
1089 origin: MetricPoint,
1090 targets: Vec<MetricPoint>,
1091 through: ObjectId,
1092 profile: MobilityProfile,
1093 tolerance_metres: f64,
1094 ) -> Result<Self, MetricRoutingError> {
1095 if targets.is_empty() {
1096 return Err(MetricRoutingError::NoTargets);
1097 }
1098 if !valid_non_negative(tolerance_metres) {
1099 return Err(MetricRoutingError::InvalidTolerance);
1100 }
1101 Ok(Self {
1102 origin,
1103 targets,
1104 through,
1105 profile,
1106 tolerance_metres,
1107 avoided: Vec::new(),
1108 connectors: None,
1109 })
1110 }
1111
1112 #[must_use]
1114 pub fn with_avoided(mut self, mut avoided: Vec<ObjectId>) -> Self {
1115 avoided.sort();
1116 avoided.dedup();
1117 self.avoided = avoided;
1118 self
1119 }
1120
1121 #[must_use]
1123 pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
1124 self.connectors = Some(connectors);
1125 self
1126 }
1127
1128 pub fn origin(&self) -> &MetricPoint {
1130 &self.origin
1131 }
1132
1133 pub fn targets(&self) -> &[MetricPoint] {
1135 &self.targets
1136 }
1137
1138 pub fn through(&self) -> &ObjectId {
1140 &self.through
1141 }
1142
1143 pub fn profile(&self) -> MobilityProfile {
1145 self.profile
1146 }
1147
1148 pub fn tolerance_metres(&self) -> f64 {
1150 self.tolerance_metres
1151 }
1152
1153 pub fn avoided(&self) -> &[ObjectId] {
1155 &self.avoided
1156 }
1157
1158 pub fn connectors(&self) -> Option<&ConnectorRouting> {
1160 self.connectors.as_ref()
1161 }
1162}
1163
1164#[derive(Clone, Debug, PartialEq)]
1166pub struct ForcedWalkEvidence {
1167 lower_metres: f64,
1168 upper_metres: f64,
1169 converged: bool,
1170 evidence: Evidence,
1171}
1172
1173impl ForcedWalkEvidence {
1174 pub fn try_new(
1184 lower_metres: f64,
1185 upper_metres: f64,
1186 converged: bool,
1187 evidence: Evidence,
1188 ) -> Result<Self, MetricRoutingError> {
1189 if !valid_non_negative(lower_metres) || upper_metres.is_nan() || upper_metres < lower_metres
1190 {
1191 return Err(MetricRoutingError::InvalidLengthInterval);
1192 }
1193 if !reviewable_exact_evidence(&evidence) {
1194 return Err(MetricRoutingError::InexactRouteEvidence);
1195 }
1196 Ok(Self {
1197 lower_metres,
1198 upper_metres,
1199 converged,
1200 evidence,
1201 })
1202 }
1203
1204 pub fn lower_metres(&self) -> f64 {
1206 self.lower_metres
1207 }
1208
1209 pub fn upper_metres(&self) -> f64 {
1212 self.upper_metres
1213 }
1214
1215 pub fn converged(&self) -> bool {
1217 self.converged
1218 }
1219
1220 pub fn evidence(&self) -> &Evidence {
1222 &self.evidence
1223 }
1224}
1225
1226#[derive(Clone, Debug, PartialEq)]
1229pub struct NeverEnteredEvidence {
1230 request: ForcedWalkRequest,
1231 completeness: CompleteMetricEvidence,
1232}
1233
1234impl NeverEnteredEvidence {
1235 pub fn new(request: ForcedWalkRequest, completeness: CompleteMetricEvidence) -> Self {
1237 Self {
1238 request,
1239 completeness,
1240 }
1241 }
1242
1243 pub fn request(&self) -> &ForcedWalkRequest {
1245 &self.request
1246 }
1247
1248 pub fn completeness(&self) -> &CompleteMetricEvidence {
1250 &self.completeness
1251 }
1252}
1253
1254#[derive(Clone, Debug, PartialEq)]
1256pub enum ForcedWalkOutcome {
1257 Bounded(ForcedWalkEvidence),
1259 NeverEntered(Box<NeverEnteredEvidence>),
1261}
1262
1263const TRACE_ROUNDING: f64 = 1e-9;
1266
1267pub trait MetricRoutingService: Send + Sync + 'static {
1269 fn route(&self, request: &MetricRouteRequest)
1271 -> Result<MetricRouteOutcome, MetricRoutingError>;
1272
1273 fn nearest_target(
1277 &self,
1278 request: &NearestTargetRequest,
1279 ) -> Result<NearestTargetOutcome, MetricRoutingError> {
1280 let _ = request;
1281 Err(MetricRoutingError::Unavailable(
1282 "this backend does not measure nearest targets".into(),
1283 ))
1284 }
1285
1286 fn farthest_point(
1289 &self,
1290 request: &FarthestPointRequest,
1291 ) -> Result<FarthestPointOutcome, MetricRoutingError> {
1292 let _ = request;
1293 Err(MetricRoutingError::Unavailable(
1294 "this backend does not measure farthest points".into(),
1295 ))
1296 }
1297
1298 fn avoids_objects(&self) -> bool {
1303 false
1304 }
1305
1306 fn weighs_travel(&self) -> bool {
1311 false
1312 }
1313
1314 fn forced_walk(
1317 &self,
1318 request: &ForcedWalkRequest,
1319 ) -> Result<ForcedWalkOutcome, MetricRoutingError> {
1320 let _ = request;
1321 Err(MetricRoutingError::Unavailable(
1322 "this backend does not measure walks forced through objects".into(),
1323 ))
1324 }
1325
1326 fn climbs_connectors(&self) -> bool {
1330 false
1331 }
1332
1333 fn trace_path(&self, request: &PathTraceRequest) -> Result<PathTrace, MetricRoutingError> {
1336 let _ = request;
1337 Err(MetricRoutingError::Unavailable(
1338 "this backend does not trace paths over objects".into(),
1339 ))
1340 }
1341}
1342
1343#[derive(Clone)]
1345pub struct MetricRoutingServiceHandle(Arc<dyn MetricRoutingService>);
1346
1347impl MetricRoutingServiceHandle {
1348 pub fn new(service: Arc<dyn MetricRoutingService>) -> Self {
1350 Self(service)
1351 }
1352
1353 pub fn route(
1355 &self,
1356 request: &MetricRouteRequest,
1357 ) -> Result<MetricRouteOutcome, MetricRoutingError> {
1358 self.climbing(request.connectors())?;
1359 let outcome = self.0.route(request)?;
1360 if let MetricRouteOutcome::Reachable(route) = &outcome {
1361 let (Some(first), Some(last)) = (route.waypoints.first(), route.waypoints.last())
1362 else {
1363 return Err(MetricRoutingError::EmptyRouteEvidence);
1364 };
1365 if first != request.origin() || last != request.destination() {
1366 return Err(MetricRoutingError::ResponseEndpointMismatch);
1367 }
1368 } else if let MetricRouteOutcome::Blocked(blocked) = &outcome
1369 && blocked.request() != request
1370 {
1371 return Err(MetricRoutingError::ResponseEndpointMismatch);
1372 }
1373 Ok(outcome)
1374 }
1375
1376 pub fn nearest_target(
1383 &self,
1384 request: &NearestTargetRequest,
1385 ) -> Result<NearestTargetOutcome, MetricRoutingError> {
1386 if !request.avoided().is_empty() && !self.0.avoids_objects() {
1387 return Err(MetricRoutingError::Unavailable(
1388 "this backend does not walk around objects".into(),
1389 ));
1390 }
1391 self.weighing(request.costs())?;
1392 self.climbing(request.connectors())?;
1393 let outcome = self.0.nearest_target(request)?;
1394 match &outcome {
1395 NearestTargetOutcome::Reached(reached) => {
1396 let target = request
1397 .targets()
1398 .get(reached.target())
1399 .ok_or(MetricRoutingError::InconsistentResponse)?;
1400 let (Some(first), Some(last)) =
1401 (reached.waypoints().first(), reached.waypoints().last())
1402 else {
1403 return Err(MetricRoutingError::EmptyRouteEvidence);
1404 };
1405 if first != request.origin() || last != target {
1406 return Err(MetricRoutingError::ResponseEndpointMismatch);
1407 }
1408 }
1409 NearestTargetOutcome::Unreachable(unreachable) => {
1410 if unreachable.request() != request {
1411 return Err(MetricRoutingError::ResponseEndpointMismatch);
1412 }
1413 }
1414 }
1415 Ok(outcome)
1416 }
1417
1418 pub fn farthest_point(
1423 &self,
1424 request: &FarthestPointRequest,
1425 ) -> Result<FarthestPointOutcome, MetricRoutingError> {
1426 self.weighing(request.costs())?;
1427 self.climbing(request.connectors())?;
1428 let outcome = self.0.farthest_point(request)?;
1429 match &outcome {
1430 FarthestPointOutcome::Bounded(bounded) => {
1431 if bounded.witness().subject() != request.region() {
1432 return Err(MetricRoutingError::ResponseEndpointMismatch);
1433 }
1434 let width = bounded.distance().upper_metres() - bounded.distance().lower_metres();
1435 if bounded.converged() && width > request.tolerance_metres() {
1436 return Err(MetricRoutingError::InconsistentResponse);
1437 }
1438 }
1439 FarthestPointOutcome::Unreachable(unreachable) => {
1440 if unreachable.request() != request
1441 || unreachable.witness().subject() != request.region()
1442 {
1443 return Err(MetricRoutingError::ResponseEndpointMismatch);
1444 }
1445 }
1446 }
1447 Ok(outcome)
1448 }
1449}
1450
1451impl MetricRoutingServiceHandle {
1452 fn weighing(&self, costs: &[TravelCost]) -> Result<(), MetricRoutingError> {
1455 if !costs.is_empty() && !self.0.weighs_travel() {
1456 return Err(MetricRoutingError::Unavailable(
1457 "this backend does not weigh travel over objects".into(),
1458 ));
1459 }
1460 Ok(())
1461 }
1462
1463 pub fn forced_walk(
1469 &self,
1470 request: &ForcedWalkRequest,
1471 ) -> Result<ForcedWalkOutcome, MetricRoutingError> {
1472 if !request.avoided().is_empty() && !self.0.avoids_objects() {
1473 return Err(MetricRoutingError::Unavailable(
1474 "this backend does not walk around objects".into(),
1475 ));
1476 }
1477 self.climbing(request.connectors())?;
1478 let outcome = self.0.forced_walk(request)?;
1479 match &outcome {
1480 ForcedWalkOutcome::Bounded(bounded) => {
1481 if bounded.converged()
1482 && bounded.upper_metres() - bounded.lower_metres() > request.tolerance_metres()
1483 {
1484 return Err(MetricRoutingError::InconsistentResponse);
1485 }
1486 }
1487 ForcedWalkOutcome::NeverEntered(never) => {
1488 if never.request() != request {
1489 return Err(MetricRoutingError::ResponseEndpointMismatch);
1490 }
1491 }
1492 }
1493 Ok(outcome)
1494 }
1495
1496 fn climbing(&self, connectors: Option<&ConnectorRouting>) -> Result<(), MetricRoutingError> {
1499 if connectors.is_some() && !self.0.climbs_connectors() {
1500 return Err(MetricRoutingError::Unavailable(
1501 "this backend does not route through vertical connectors".into(),
1502 ));
1503 }
1504 Ok(())
1505 }
1506
1507 pub fn trace_path(&self, request: &PathTraceRequest) -> Result<PathTrace, MetricRoutingError> {
1511 let trace = self.0.trace_path(request)?;
1512 if trace.lengths().len() != request.objects().len() {
1513 return Err(MetricRoutingError::InconsistentResponse);
1514 }
1515 let most = request.plan_length_metres() * (1.0 + TRACE_ROUNDING) + TRACE_ROUNDING;
1516 if trace
1517 .lengths()
1518 .iter()
1519 .flatten()
1520 .any(|length| length.lower_metres() > most)
1521 {
1522 return Err(MetricRoutingError::InconsistentResponse);
1523 }
1524 Ok(trace)
1525 }
1526}
1527
1528fn valid_non_negative(value: f64) -> bool {
1529 value.is_finite() && value >= 0.0
1530}