1use std::sync::Arc;
8
9use crate::services::reviewable_exact_evidence;
10use axioval_ir::{Evidence, ObjectId};
11use thiserror::Error;
12
13#[derive(Clone, Debug, Error, PartialEq, Eq)]
15pub enum MetricRoutingError {
16 #[error("metric point coordinates must be finite")]
18 InvalidCoordinate,
19 #[error("metric length interval is invalid")]
21 InvalidLengthInterval,
22 #[error("mobility profile contains an invalid dimension")]
24 InvalidMobilityProfile,
25 #[error("metric route evidence is empty")]
27 EmptyRouteEvidence,
28 #[error("metric route provenance is not exact and reviewable")]
30 InexactRouteEvidence,
31 #[error("metric evidence is incomplete")]
33 IncompleteMetricEvidence,
34 #[error("metric routing backend returned mismatched endpoints")]
36 ResponseEndpointMismatch,
37 #[error("metric geometry is unavailable for `{0}`")]
39 MissingGeometry(Box<ObjectId>),
40 #[error("metric routing query unavailable: {0}")]
42 Unavailable(String),
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum ThresholdVerdict {
48 Satisfied,
50 Violated,
52 Indeterminate,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct LengthInterval {
59 lower_metres: f64,
60 upper_metres: f64,
61}
62
63impl LengthInterval {
64 pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, MetricRoutingError> {
66 if !valid_non_negative(lower_metres)
67 || !valid_non_negative(upper_metres)
68 || lower_metres > upper_metres
69 {
70 return Err(MetricRoutingError::InvalidLengthInterval);
71 }
72 Ok(Self {
73 lower_metres,
74 upper_metres,
75 })
76 }
77
78 pub fn exact(metres: f64) -> Result<Self, MetricRoutingError> {
80 Self::try_new(metres, metres)
81 }
82
83 pub fn lower_metres(&self) -> f64 {
85 self.lower_metres
86 }
87
88 pub fn upper_metres(&self) -> f64 {
90 self.upper_metres
91 }
92
93 #[allow(clippy::float_cmp)]
95 pub fn is_exact(&self) -> bool {
96 self.lower_metres == self.upper_metres
99 }
100
101 pub fn compare_maximum(
103 &self,
104 maximum_metres: f64,
105 ) -> Result<ThresholdVerdict, MetricRoutingError> {
106 if !valid_non_negative(maximum_metres) {
107 return Err(MetricRoutingError::InvalidLengthInterval);
108 }
109 if self.upper_metres <= maximum_metres {
110 Ok(ThresholdVerdict::Satisfied)
111 } else if self.lower_metres > maximum_metres {
112 Ok(ThresholdVerdict::Violated)
113 } else {
114 Ok(ThresholdVerdict::Indeterminate)
115 }
116 }
117}
118
119#[derive(Clone, Debug, PartialEq)]
121pub struct MetricPoint {
122 subject: ObjectId,
123 coordinates_metres: [f64; 3],
124}
125
126impl MetricPoint {
127 pub fn try_new(
129 subject: ObjectId,
130 coordinates_metres: [f64; 3],
131 ) -> Result<Self, MetricRoutingError> {
132 if !coordinates_metres.iter().all(|value| value.is_finite()) {
133 return Err(MetricRoutingError::InvalidCoordinate);
134 }
135 Ok(Self {
136 subject,
137 coordinates_metres,
138 })
139 }
140
141 pub fn subject(&self) -> &ObjectId {
143 &self.subject
144 }
145
146 pub fn coordinates_metres(&self) -> [f64; 3] {
148 self.coordinates_metres
149 }
150}
151
152#[derive(Clone, Copy, Debug, PartialEq)]
154pub struct MobilityProfile {
155 radius_metres: f64,
156 height_metres: f64,
157 maximum_step_metres: f64,
158 maximum_slope: f64,
159}
160
161impl MobilityProfile {
162 pub fn try_new(
164 radius_metres: f64,
165 height_metres: f64,
166 maximum_step_metres: f64,
167 maximum_slope: f64,
168 ) -> Result<Self, MetricRoutingError> {
169 if ![
170 radius_metres,
171 height_metres,
172 maximum_step_metres,
173 maximum_slope,
174 ]
175 .into_iter()
176 .all(valid_non_negative)
177 {
178 return Err(MetricRoutingError::InvalidMobilityProfile);
179 }
180 Ok(Self {
181 radius_metres,
182 height_metres,
183 maximum_step_metres,
184 maximum_slope,
185 })
186 }
187
188 pub fn radius_metres(&self) -> f64 {
190 self.radius_metres
191 }
192
193 pub fn height_metres(&self) -> f64 {
195 self.height_metres
196 }
197
198 pub fn maximum_step_metres(&self) -> f64 {
200 self.maximum_step_metres
201 }
202
203 pub fn maximum_slope(&self) -> f64 {
205 self.maximum_slope
206 }
207}
208
209#[derive(Clone, Debug, PartialEq)]
211pub struct MetricRouteRequest {
212 origin: MetricPoint,
213 destination: MetricPoint,
214 profile: MobilityProfile,
215}
216
217impl MetricRouteRequest {
218 pub fn new(origin: MetricPoint, destination: MetricPoint, profile: MobilityProfile) -> Self {
220 Self {
221 origin,
222 destination,
223 profile,
224 }
225 }
226
227 pub fn origin(&self) -> &MetricPoint {
229 &self.origin
230 }
231
232 pub fn destination(&self) -> &MetricPoint {
234 &self.destination
235 }
236
237 pub fn profile(&self) -> MobilityProfile {
239 self.profile
240 }
241}
242
243#[derive(Clone, Debug, PartialEq, Eq)]
245pub struct CompleteMetricEvidence(Evidence);
246
247impl CompleteMetricEvidence {
248 pub fn try_new(evidence: Evidence) -> Result<Self, MetricRoutingError> {
250 if !reviewable_exact_evidence(&evidence) {
251 return Err(MetricRoutingError::IncompleteMetricEvidence);
252 }
253 Ok(Self(evidence))
254 }
255
256 pub fn evidence(&self) -> &Evidence {
258 &self.0
259 }
260}
261
262#[derive(Clone, Debug, PartialEq)]
264pub struct BlockedMetricRouteEvidence {
265 request: MetricRouteRequest,
266 completeness: CompleteMetricEvidence,
267}
268
269impl BlockedMetricRouteEvidence {
270 pub fn new(request: MetricRouteRequest, completeness: CompleteMetricEvidence) -> Self {
272 Self {
273 request,
274 completeness,
275 }
276 }
277
278 pub fn request(&self) -> &MetricRouteRequest {
280 &self.request
281 }
282
283 pub fn completeness(&self) -> &CompleteMetricEvidence {
285 &self.completeness
286 }
287}
288
289#[derive(Clone, Debug, PartialEq)]
291pub struct MetricRouteEvidence {
292 shortest_distance: LengthInterval,
293 waypoints: Vec<MetricPoint>,
294 traversed_objects: Vec<ObjectId>,
295 evidence: Evidence,
296}
297
298impl MetricRouteEvidence {
299 pub fn try_new(
301 shortest_distance: LengthInterval,
302 waypoints: Vec<MetricPoint>,
303 traversed_objects: Vec<ObjectId>,
304 evidence: Evidence,
305 ) -> Result<Self, MetricRoutingError> {
306 if waypoints.is_empty() || traversed_objects.is_empty() {
307 return Err(MetricRoutingError::EmptyRouteEvidence);
308 }
309 if !reviewable_exact_evidence(&evidence) {
310 return Err(MetricRoutingError::InexactRouteEvidence);
311 }
312 Ok(Self {
313 shortest_distance,
314 waypoints,
315 traversed_objects,
316 evidence,
317 })
318 }
319
320 pub fn shortest_distance(&self) -> &LengthInterval {
322 &self.shortest_distance
323 }
324
325 pub fn waypoints(&self) -> &[MetricPoint] {
327 &self.waypoints
328 }
329
330 pub fn traversed_objects(&self) -> &[ObjectId] {
332 &self.traversed_objects
333 }
334
335 pub fn evidence(&self) -> &Evidence {
337 &self.evidence
338 }
339}
340
341#[derive(Clone, Debug, PartialEq)]
343pub enum MetricRouteOutcome {
344 Reachable(MetricRouteEvidence),
346 Blocked(BlockedMetricRouteEvidence),
348}
349
350pub trait MetricRoutingService: Send + Sync + 'static {
352 fn route(&self, request: &MetricRouteRequest)
354 -> Result<MetricRouteOutcome, MetricRoutingError>;
355}
356
357#[derive(Clone)]
359pub struct MetricRoutingServiceHandle(Arc<dyn MetricRoutingService>);
360
361impl MetricRoutingServiceHandle {
362 pub fn new(service: Arc<dyn MetricRoutingService>) -> Self {
364 Self(service)
365 }
366
367 pub fn route(
369 &self,
370 request: &MetricRouteRequest,
371 ) -> Result<MetricRouteOutcome, MetricRoutingError> {
372 let outcome = self.0.route(request)?;
373 if let MetricRouteOutcome::Reachable(route) = &outcome {
374 let (Some(first), Some(last)) = (route.waypoints.first(), route.waypoints.last())
375 else {
376 return Err(MetricRoutingError::EmptyRouteEvidence);
377 };
378 if first != request.origin() || last != request.destination() {
379 return Err(MetricRoutingError::ResponseEndpointMismatch);
380 }
381 } else if let MetricRouteOutcome::Blocked(blocked) = &outcome
382 && blocked.request() != request
383 {
384 return Err(MetricRoutingError::ResponseEndpointMismatch);
385 }
386 Ok(outcome)
387 }
388}
389
390fn valid_non_negative(value: f64) -> bool {
391 value.is_finite() && value >= 0.0
392}