1use std::sync::Arc;
8
9use axioval_ir::{Evidence, ObjectId};
10use thiserror::Error;
11
12#[derive(Clone, Debug, Error, PartialEq, Eq)]
14pub enum MetricRoutingError {
15 #[error("metric point coordinates must be finite")]
17 InvalidCoordinate,
18 #[error("metric length interval is invalid")]
20 InvalidLengthInterval,
21 #[error("mobility profile contains an invalid dimension")]
23 InvalidMobilityProfile,
24 #[error("metric route evidence is empty")]
26 EmptyRouteEvidence,
27 #[error("metric route provenance is not exact and reviewable")]
29 InexactRouteEvidence,
30 #[error("metric evidence is incomplete")]
32 IncompleteMetricEvidence,
33 #[error("metric routing backend returned mismatched endpoints")]
35 ResponseEndpointMismatch,
36 #[error("metric geometry is unavailable for `{0}`")]
38 MissingGeometry(Box<ObjectId>),
39 #[error("metric routing query unavailable: {0}")]
41 Unavailable(String),
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum ThresholdVerdict {
47 Satisfied,
49 Violated,
51 Indeterminate,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct LengthInterval {
58 lower_metres: f64,
59 upper_metres: f64,
60}
61
62impl LengthInterval {
63 pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, MetricRoutingError> {
65 if !valid_non_negative(lower_metres)
66 || !valid_non_negative(upper_metres)
67 || lower_metres > upper_metres
68 {
69 return Err(MetricRoutingError::InvalidLengthInterval);
70 }
71 Ok(Self {
72 lower_metres,
73 upper_metres,
74 })
75 }
76
77 pub fn exact(metres: f64) -> Result<Self, MetricRoutingError> {
79 Self::try_new(metres, metres)
80 }
81
82 pub fn lower_metres(&self) -> f64 {
84 self.lower_metres
85 }
86
87 pub fn upper_metres(&self) -> f64 {
89 self.upper_metres
90 }
91
92 #[allow(clippy::float_cmp)]
94 pub fn is_exact(&self) -> bool {
95 self.lower_metres == self.upper_metres
98 }
99
100 pub fn compare_maximum(
102 &self,
103 maximum_metres: f64,
104 ) -> Result<ThresholdVerdict, MetricRoutingError> {
105 if !valid_non_negative(maximum_metres) {
106 return Err(MetricRoutingError::InvalidLengthInterval);
107 }
108 if self.upper_metres <= maximum_metres {
109 Ok(ThresholdVerdict::Satisfied)
110 } else if self.lower_metres > maximum_metres {
111 Ok(ThresholdVerdict::Violated)
112 } else {
113 Ok(ThresholdVerdict::Indeterminate)
114 }
115 }
116}
117
118#[derive(Clone, Debug, PartialEq)]
120pub struct MetricPoint {
121 subject: ObjectId,
122 coordinates_metres: [f64; 3],
123}
124
125impl MetricPoint {
126 pub fn try_new(
128 subject: ObjectId,
129 coordinates_metres: [f64; 3],
130 ) -> Result<Self, MetricRoutingError> {
131 if !coordinates_metres.iter().all(|value| value.is_finite()) {
132 return Err(MetricRoutingError::InvalidCoordinate);
133 }
134 Ok(Self {
135 subject,
136 coordinates_metres,
137 })
138 }
139
140 pub fn subject(&self) -> &ObjectId {
142 &self.subject
143 }
144
145 pub fn coordinates_metres(&self) -> [f64; 3] {
147 self.coordinates_metres
148 }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct MobilityProfile {
154 radius_metres: f64,
155 height_metres: f64,
156 maximum_step_metres: f64,
157 maximum_slope: f64,
158}
159
160impl MobilityProfile {
161 pub fn try_new(
163 radius_metres: f64,
164 height_metres: f64,
165 maximum_step_metres: f64,
166 maximum_slope: f64,
167 ) -> Result<Self, MetricRoutingError> {
168 if ![
169 radius_metres,
170 height_metres,
171 maximum_step_metres,
172 maximum_slope,
173 ]
174 .into_iter()
175 .all(valid_non_negative)
176 {
177 return Err(MetricRoutingError::InvalidMobilityProfile);
178 }
179 Ok(Self {
180 radius_metres,
181 height_metres,
182 maximum_step_metres,
183 maximum_slope,
184 })
185 }
186
187 pub fn radius_metres(&self) -> f64 {
189 self.radius_metres
190 }
191
192 pub fn height_metres(&self) -> f64 {
194 self.height_metres
195 }
196
197 pub fn maximum_step_metres(&self) -> f64 {
199 self.maximum_step_metres
200 }
201
202 pub fn maximum_slope(&self) -> f64 {
204 self.maximum_slope
205 }
206}
207
208#[derive(Clone, Debug, PartialEq)]
210pub struct MetricRouteRequest {
211 origin: MetricPoint,
212 destination: MetricPoint,
213 profile: MobilityProfile,
214}
215
216impl MetricRouteRequest {
217 pub fn new(origin: MetricPoint, destination: MetricPoint, profile: MobilityProfile) -> Self {
219 Self {
220 origin,
221 destination,
222 profile,
223 }
224 }
225
226 pub fn origin(&self) -> &MetricPoint {
228 &self.origin
229 }
230
231 pub fn destination(&self) -> &MetricPoint {
233 &self.destination
234 }
235
236 pub fn profile(&self) -> MobilityProfile {
238 self.profile
239 }
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct CompleteMetricEvidence(Evidence);
245
246impl CompleteMetricEvidence {
247 pub fn try_new(evidence: Evidence) -> Result<Self, MetricRoutingError> {
249 if !reviewable_exact_evidence(&evidence) {
250 return Err(MetricRoutingError::IncompleteMetricEvidence);
251 }
252 Ok(Self(evidence))
253 }
254
255 pub fn evidence(&self) -> &Evidence {
257 &self.0
258 }
259}
260
261#[derive(Clone, Debug, PartialEq)]
263pub struct BlockedMetricRouteEvidence {
264 request: MetricRouteRequest,
265 completeness: CompleteMetricEvidence,
266}
267
268impl BlockedMetricRouteEvidence {
269 pub fn new(request: MetricRouteRequest, completeness: CompleteMetricEvidence) -> Self {
271 Self {
272 request,
273 completeness,
274 }
275 }
276
277 pub fn request(&self) -> &MetricRouteRequest {
279 &self.request
280 }
281
282 pub fn completeness(&self) -> &CompleteMetricEvidence {
284 &self.completeness
285 }
286}
287
288#[derive(Clone, Debug, PartialEq)]
290pub struct MetricRouteEvidence {
291 shortest_distance: LengthInterval,
292 waypoints: Vec<MetricPoint>,
293 traversed_objects: Vec<ObjectId>,
294 evidence: Evidence,
295}
296
297impl MetricRouteEvidence {
298 pub fn try_new(
300 shortest_distance: LengthInterval,
301 waypoints: Vec<MetricPoint>,
302 traversed_objects: Vec<ObjectId>,
303 evidence: Evidence,
304 ) -> Result<Self, MetricRoutingError> {
305 if waypoints.is_empty() || traversed_objects.is_empty() {
306 return Err(MetricRoutingError::EmptyRouteEvidence);
307 }
308 if !reviewable_exact_evidence(&evidence) {
309 return Err(MetricRoutingError::InexactRouteEvidence);
310 }
311 Ok(Self {
312 shortest_distance,
313 waypoints,
314 traversed_objects,
315 evidence,
316 })
317 }
318
319 pub fn shortest_distance(&self) -> &LengthInterval {
321 &self.shortest_distance
322 }
323
324 pub fn waypoints(&self) -> &[MetricPoint] {
326 &self.waypoints
327 }
328
329 pub fn traversed_objects(&self) -> &[ObjectId] {
331 &self.traversed_objects
332 }
333
334 pub fn evidence(&self) -> &Evidence {
336 &self.evidence
337 }
338}
339
340#[derive(Clone, Debug, PartialEq)]
342pub enum MetricRouteOutcome {
343 Reachable(MetricRouteEvidence),
345 Blocked(BlockedMetricRouteEvidence),
347}
348
349pub trait MetricRoutingService: Send + Sync + 'static {
351 fn route(&self, request: &MetricRouteRequest)
353 -> Result<MetricRouteOutcome, MetricRoutingError>;
354}
355
356#[derive(Clone)]
358pub struct MetricRoutingServiceHandle(Arc<dyn MetricRoutingService>);
359
360impl MetricRoutingServiceHandle {
361 pub fn new(service: Arc<dyn MetricRoutingService>) -> Self {
363 Self(service)
364 }
365
366 pub fn route(
368 &self,
369 request: &MetricRouteRequest,
370 ) -> Result<MetricRouteOutcome, MetricRoutingError> {
371 let outcome = self.0.route(request)?;
372 if let MetricRouteOutcome::Reachable(route) = &outcome {
373 let (Some(first), Some(last)) = (route.waypoints.first(), route.waypoints.last())
374 else {
375 return Err(MetricRoutingError::EmptyRouteEvidence);
376 };
377 if first != request.origin() || last != request.destination() {
378 return Err(MetricRoutingError::ResponseEndpointMismatch);
379 }
380 } else if let MetricRouteOutcome::Blocked(blocked) = &outcome
381 && blocked.request() != request
382 {
383 return Err(MetricRoutingError::ResponseEndpointMismatch);
384 }
385 Ok(outcome)
386 }
387}
388
389fn valid_non_negative(value: f64) -> bool {
390 value.is_finite() && value >= 0.0
391}
392
393fn reviewable_exact_evidence(evidence: &Evidence) -> bool {
394 evidence.exact && !evidence.locator.trim().is_empty()
395}