1use crate::{LengthInterval, ServiceRegistry, ServiceRegistryError};
3use axioval_ir::{Evidence, ObjectId};
4use std::{
5 collections::{BTreeMap, BTreeSet, VecDeque},
6 sync::Arc,
7};
8use thiserror::Error;
9#[derive(Clone, Debug, Error, PartialEq)]
10pub enum WalkabilityError {
11 #[error("minimum width must be finite and positive")]
12 InvalidMinimumWidth,
13 #[error("walkability region identifier is blank")]
14 InvalidRegionId,
15 #[error("passage joins a region to itself")]
16 SelfPassage,
17 #[error("passage evidence is not exact and reviewable")]
18 InexactPassage,
19 #[error("walkability evidence is incomplete")]
20 IncompleteEvidence,
21 #[error("duplicate walkability region")]
22 DuplicateRegion,
23 #[error("passage names an unknown region")]
24 UnknownRegion,
25 #[error("duplicate walkable passage")]
26 DuplicatePassage,
27 #[error("region maps an object outside the request universe")]
28 UnexpectedMappedObject,
29 #[error("portal passage violates the request portal policy")]
30 ForbiddenPortalPassage,
31 #[error("walkability object is not mapped to a region")]
32 ObjectUnavailable,
33 #[error("backend returned another request")]
34 ResponseRequestMismatch,
35}
36#[derive(Clone, Debug, PartialEq)]
37pub struct WalkabilityRequest {
38 surfaces: Vec<ObjectId>,
39 entrances: Vec<ObjectId>,
40 obstacles: Vec<ObjectId>,
41 minimum_width: f64,
42 elevation_band: Option<LengthInterval>,
43 traverse_verified_portals: bool,
44 include_motion_envelopes: bool,
45}
46impl WalkabilityRequest {
47 pub fn try_new(
48 mut surfaces: Vec<ObjectId>,
49 mut entrances: Vec<ObjectId>,
50 mut obstacles: Vec<ObjectId>,
51 minimum_width: f64,
52 elevation_band: Option<LengthInterval>,
53 traverse_verified_portals: bool,
54 include_motion_envelopes: bool,
55 ) -> Result<Self, WalkabilityError> {
56 if !minimum_width.is_finite() || minimum_width <= 0.0 {
57 return Err(WalkabilityError::InvalidMinimumWidth);
58 }
59 surfaces.sort();
60 surfaces.dedup();
61 entrances.sort();
62 entrances.dedup();
63 obstacles.sort();
64 obstacles.dedup();
65 Ok(Self {
66 surfaces,
67 entrances,
68 obstacles,
69 minimum_width,
70 elevation_band,
71 traverse_verified_portals,
72 include_motion_envelopes,
73 })
74 }
75 pub fn surfaces(&self) -> &[ObjectId] {
76 &self.surfaces
77 }
78 pub fn entrances(&self) -> &[ObjectId] {
79 &self.entrances
80 }
81 pub fn obstacles(&self) -> &[ObjectId] {
82 &self.obstacles
83 }
84 pub fn minimum_width_metres(&self) -> f64 {
85 self.minimum_width
86 }
87 pub fn elevation_band(&self) -> Option<LengthInterval> {
88 self.elevation_band
89 }
90 pub fn traverses_verified_portals(&self) -> bool {
91 self.traverse_verified_portals
92 }
93 pub fn includes_motion_envelopes(&self) -> bool {
94 self.include_motion_envelopes
95 }
96}
97#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
98pub struct WalkabilityRegionId(String);
99impl WalkabilityRegionId {
100 pub fn new(value: impl Into<String>) -> Result<Self, WalkabilityError> {
101 let value = value.into();
102 if value.trim().is_empty() {
103 Err(WalkabilityError::InvalidRegionId)
104 } else {
105 Ok(Self(value))
106 }
107 }
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111}
112#[derive(Clone, Debug, PartialEq)]
113pub struct WalkabilityRegion {
114 id: WalkabilityRegionId,
115 objects: Vec<ObjectId>,
116}
117impl WalkabilityRegion {
118 pub fn new(id: WalkabilityRegionId, mut objects: Vec<ObjectId>) -> Self {
119 objects.sort();
120 objects.dedup();
121 Self { id, objects }
122 }
123 pub fn id(&self) -> &WalkabilityRegionId {
124 &self.id
125 }
126 pub fn objects(&self) -> &[ObjectId] {
127 &self.objects
128 }
129}
130#[derive(Clone, Debug, PartialEq)]
131pub struct VerifiedWalkablePassage {
132 a: WalkabilityRegionId,
133 b: WalkabilityRegionId,
134 portal: Option<ObjectId>,
135 clear_width: LengthInterval,
136 evidence: Evidence,
137}
138impl VerifiedWalkablePassage {
139 pub fn try_new(
140 mut a: WalkabilityRegionId,
141 mut b: WalkabilityRegionId,
142 portal: Option<ObjectId>,
143 clear_width: LengthInterval,
144 evidence: Evidence,
145 ) -> Result<Self, WalkabilityError> {
146 if a == b {
147 return Err(WalkabilityError::SelfPassage);
148 }
149 if !evidence.exact || evidence.locator.trim().is_empty() {
150 return Err(WalkabilityError::InexactPassage);
151 }
152 if b < a {
153 std::mem::swap(&mut a, &mut b);
154 }
155 Ok(Self {
156 a,
157 b,
158 portal,
159 clear_width,
160 evidence,
161 })
162 }
163 pub fn endpoints(&self) -> (&WalkabilityRegionId, &WalkabilityRegionId) {
164 (&self.a, &self.b)
165 }
166 pub fn portal(&self) -> Option<&ObjectId> {
167 self.portal.as_ref()
168 }
169 pub fn clear_width(&self) -> LengthInterval {
170 self.clear_width
171 }
172 pub fn evidence(&self) -> &Evidence {
173 &self.evidence
174 }
175}
176#[derive(Clone, Debug, PartialEq)]
177pub struct WalkabilitySnapshot {
178 request: WalkabilityRequest,
179 regions: Vec<WalkabilityRegion>,
180 passages: Vec<VerifiedWalkablePassage>,
181 object_regions: BTreeMap<ObjectId, Vec<WalkabilityRegionId>>,
182 evidence: Evidence,
183}
184impl WalkabilitySnapshot {
185 pub fn try_new(
186 request: WalkabilityRequest,
187 mut regions: Vec<WalkabilityRegion>,
188 mut passages: Vec<VerifiedWalkablePassage>,
189 evidence: Evidence,
190 ) -> Result<Self, WalkabilityError> {
191 if !evidence.exact || evidence.locator.trim().is_empty() {
192 return Err(WalkabilityError::IncompleteEvidence);
193 }
194 regions.sort_by(|a, b| a.id.cmp(&b.id));
195 if regions.windows(2).any(|w| w[0].id == w[1].id) {
196 return Err(WalkabilityError::DuplicateRegion);
197 }
198 let ids: BTreeSet<_> = regions.iter().map(|r| r.id.clone()).collect();
199 if passages
200 .iter()
201 .any(|p| !ids.contains(&p.a) || !ids.contains(&p.b))
202 {
203 return Err(WalkabilityError::UnknownRegion);
204 }
205 let universe: BTreeSet<_> = request
206 .surfaces()
207 .iter()
208 .chain(request.entrances())
209 .chain(request.obstacles())
210 .cloned()
211 .collect();
212 if regions
213 .iter()
214 .flat_map(|region| region.objects.iter())
215 .any(|object| !universe.contains(object))
216 {
217 return Err(WalkabilityError::UnexpectedMappedObject);
218 }
219 if passages.iter().any(|passage| {
220 passage.portal.as_ref().is_some_and(|portal| {
221 !request.traverse_verified_portals
222 || request.entrances.binary_search(portal).is_err()
223 })
224 }) {
225 return Err(WalkabilityError::ForbiddenPortalPassage);
226 }
227 passages.sort_by(|a, b| (&a.a, &a.b, &a.portal).cmp(&(&b.a, &b.b, &b.portal)));
228 if passages.windows(2).any(|window| {
229 (&window[0].a, &window[0].b, &window[0].portal)
230 == (&window[1].a, &window[1].b, &window[1].portal)
231 }) {
232 return Err(WalkabilityError::DuplicatePassage);
233 }
234 let mut object_regions: BTreeMap<ObjectId, Vec<WalkabilityRegionId>> = BTreeMap::new();
235 for region in ®ions {
236 for object in ®ion.objects {
237 object_regions
238 .entry(object.clone())
239 .or_default()
240 .push(region.id.clone());
241 }
242 }
243 for mapped in object_regions.values_mut() {
244 mapped.sort();
245 mapped.dedup();
246 }
247 Ok(Self {
248 request,
249 regions,
250 passages,
251 object_regions,
252 evidence,
253 })
254 }
255 pub fn request(&self) -> &WalkabilityRequest {
256 &self.request
257 }
258 pub fn regions(&self) -> &[WalkabilityRegion] {
259 &self.regions
260 }
261 pub fn passages(&self) -> &[VerifiedWalkablePassage] {
262 &self.passages
263 }
264 pub fn evidence(&self) -> &Evidence {
265 &self.evidence
266 }
267 pub fn route_between(
268 &self,
269 from: &ObjectId,
270 to: &ObjectId,
271 ) -> Result<WalkabilityRouteOutcome, WalkabilityError> {
272 let starts = self
273 .object_regions
274 .get(from)
275 .ok_or(WalkabilityError::ObjectUnavailable)?;
276 let goals = self
277 .object_regions
278 .get(to)
279 .ok_or(WalkabilityError::ObjectUnavailable)?;
280 if let Some(path) = self.path(starts, goals, false) {
281 return Ok(WalkabilityRouteOutcome::Reachable(path));
282 }
283 if self.path(starts, goals, true).is_some() {
284 Ok(WalkabilityRouteOutcome::Indeterminate)
285 } else {
286 Ok(WalkabilityRouteOutcome::Unreachable)
287 }
288 }
289 fn path(
290 &self,
291 starts: &[WalkabilityRegionId],
292 goals: &[WalkabilityRegionId],
293 possible: bool,
294 ) -> Option<Vec<WalkabilityRegionId>> {
295 let mut graph: BTreeMap<WalkabilityRegionId, Vec<WalkabilityRegionId>> = BTreeMap::new();
296 for edge in &self.passages {
297 let usable = if possible {
298 edge.clear_width.upper_metres() >= self.request.minimum_width
299 } else {
300 edge.clear_width.lower_metres() >= self.request.minimum_width
301 };
302 if usable {
303 graph
304 .entry(edge.a.clone())
305 .or_default()
306 .push(edge.b.clone());
307 graph
308 .entry(edge.b.clone())
309 .or_default()
310 .push(edge.a.clone());
311 }
312 }
313 for neighbors in graph.values_mut() {
314 neighbors.sort();
315 neighbors.dedup();
316 }
317 let goal_set: BTreeSet<_> = goals.iter().cloned().collect();
318 let mut queue = VecDeque::new();
319 let mut parent: BTreeMap<WalkabilityRegionId, Option<WalkabilityRegionId>> =
320 BTreeMap::new();
321 for start in starts {
322 if parent.insert(start.clone(), None).is_none() {
323 queue.push_back(start.clone());
324 }
325 }
326 while let Some(node) = queue.pop_front() {
327 if goal_set.contains(&node) {
328 let mut path = vec![node.clone()];
329 let mut cursor = node;
330 while let Some(Some(prev)) = parent.get(&cursor) {
331 path.push(prev.clone());
332 cursor = prev.clone();
333 }
334 path.reverse();
335 return Some(path);
336 }
337 for next in graph.get(&node).into_iter().flatten() {
338 if !parent.contains_key(next) {
339 parent.insert(next.clone(), Some(node.clone()));
340 queue.push_back(next.clone());
341 }
342 }
343 }
344 None
345 }
346}
347#[derive(Clone, Debug, PartialEq)]
348pub enum WalkabilityRouteOutcome {
349 Reachable(Vec<WalkabilityRegionId>),
350 Unreachable,
351 Indeterminate,
352}
353pub trait WalkabilityService: Send + Sync {
354 fn snapshot(
355 &self,
356 request: &WalkabilityRequest,
357 ) -> Result<WalkabilitySnapshot, WalkabilityError>;
358}
359#[derive(Clone)]
360pub struct WalkabilityServiceHandle(Arc<dyn WalkabilityService>);
361impl WalkabilityServiceHandle {
362 pub fn new(service: Arc<dyn WalkabilityService>) -> Self {
363 Self(service)
364 }
365 pub fn snapshot(
366 &self,
367 request: &WalkabilityRequest,
368 ) -> Result<WalkabilitySnapshot, WalkabilityError> {
369 let snapshot = self.0.snapshot(request)?;
370 if snapshot.request() != request {
371 return Err(WalkabilityError::ResponseRequestMismatch);
372 }
373 Ok(snapshot)
374 }
375 pub fn register(self, services: &mut ServiceRegistry) -> Result<(), ServiceRegistryError> {
376 services.register(self)
377 }
378}