1use crate::{
2 Access, Coord, Edge, EdgeAttr, EdgeId, EdgeIndex, EdgeProjection, GradeDistribution,
3 HikingModel, LineString, LoopConstraints, Route, RouteShape, Terrain, TrailgenError, TurnBan,
4 Vertex, VertexId, WalkGraph,
5};
6use serde::{Deserialize, Serialize};
7use std::{
8 collections::{BTreeMap, BTreeSet},
9 sync::Arc,
10};
11
12pub const DEFAULT_ROAD_AVERSION: f64 = 2.0;
13
14#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct SupportPoint(Coord);
17
18impl SupportPoint {
19 pub fn forge(coord: Coord) -> Option<Self> {
20 (coord.lon.is_finite()
21 && coord.lat.is_finite()
22 && (-180.0..=180.0).contains(&coord.lon)
23 && (-85.0..=85.0).contains(&coord.lat)
24 && coord.ele.is_none_or(f64::is_finite))
25 .then_some(Self(coord))
26 }
27
28 #[must_use]
29 pub const fn coord(self) -> Coord {
30 self.0
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct RoutingLaw {
37 #[serde(default = "default_road_aversion", alias = "road_penalty")]
38 pub road_aversion: f64,
39}
40
41impl Default for RoutingLaw {
42 fn default() -> Self {
43 Self {
44 road_aversion: DEFAULT_ROAD_AVERSION,
45 }
46 }
47}
48
49impl RoutingLaw {
50 pub fn validate(self) -> crate::Result<()> {
51 if !self.road_aversion.is_finite() || self.road_aversion < 0.0 {
52 return Err(TrailgenError::InvalidData(
53 "road aversion must be finite and nonnegative".to_owned(),
54 ));
55 }
56 Ok(())
57 }
58
59 #[must_use]
60 pub fn edge_cost(self, graph: &WalkGraph, edge: EdgeId) -> Option<f64> {
61 let attr = &graph.edges[edge.0].attr;
62 if matches!(attr.access, Access::Closed | Access::Private) {
63 return None;
64 }
65 let road =
66 attr.road_exposure
67 .clamp(0.0, 1.0)
68 .max(if matches!(attr.terrain, Terrain::Road) {
69 1.0
70 } else {
71 0.0
72 });
73 Some(attr.length_m * self.road_aversion.mul_add(road, 1.0))
74 }
75}
76
77const fn default_road_aversion() -> f64 {
78 DEFAULT_ROAD_AVERSION
79}
80
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct Trail {
84 pub shape: RouteShape,
85 pub support_points: Vec<SupportPoint>,
86 #[serde(default)]
87 pub routing: RoutingLaw,
88}
89
90impl Trail {
91 pub fn forge(
92 shape: RouteShape,
93 support_points: Vec<SupportPoint>,
94 routing: RoutingLaw,
95 ) -> crate::Result<Self> {
96 let trail = Self {
97 shape,
98 support_points,
99 routing,
100 };
101 trail.validate()?;
102 Ok(trail)
103 }
104
105 pub fn validate(&self) -> crate::Result<()> {
106 self.routing.validate()?;
107 if self.shape == RouteShape::FigureEight {
108 return Err(TrailgenError::InvalidData(
109 "figure-eight support topology is not defined".to_owned(),
110 ));
111 }
112 if self.support_points.len() < 2 {
113 return Err(TrailgenError::InvalidData(
114 "a trail needs a trailhead and at least one further support point".to_owned(),
115 ));
116 }
117 if self
118 .support_points
119 .iter()
120 .any(|point| SupportPoint::forge(point.coord()).is_none())
121 {
122 return Err(TrailgenError::InvalidData(
123 "trail contains an invalid support point".to_owned(),
124 ));
125 }
126 Ok(())
127 }
128
129 #[must_use]
133 pub fn infer(graph: &WalkGraph, route: &Route, routing: RoutingLaw) -> Option<Self> {
134 routing.validate().ok()?;
135 let vertices = route_vertices(graph, route)?;
136 let n = route.edges.len();
137 if n == 0 {
138 return None;
139 }
140 let points = match route.metrics.shape {
141 RouteShape::OutAndBack => {
142 let split = n / 2;
143 if !n.is_multiple_of(2)
144 || route.edges[..split]
145 != route.edges[split..]
146 .iter()
147 .rev()
148 .copied()
149 .collect::<Vec<_>>()
150 {
151 return None;
152 }
153 let mut points = Vec::new();
154 compress_arc(graph, route, &vertices, routing, 0, split, &mut points)?;
155 points.push(vertex_support(graph, vertices[split])?);
156 points
157 }
158 RouteShape::Loop => {
159 if vertices[n] != route.start || n < 2 {
160 return None;
161 }
162 let mut points = Vec::new();
163 let split = n / 2;
164 compress_arc(graph, route, &vertices, routing, 0, split, &mut points)?;
165 compress_arc(graph, route, &vertices, routing, split, n, &mut points)?;
166 points
167 }
168 RouteShape::Open => {
169 let mut points = Vec::new();
170 compress_arc(graph, route, &vertices, routing, 0, n, &mut points)?;
171 points.push(vertex_support(graph, vertices[n])?);
172 points
173 }
174 RouteShape::FigureEight => return None,
175 };
176 Self::forge(route.metrics.shape, points, routing).ok()
177 }
178
179 pub fn realize(
180 &self,
181 name: impl Into<String>,
182 graph: &WalkGraph,
183 constraints: &LoopConstraints,
184 max_snap_m: f64,
185 ) -> crate::Result<TrailRealization> {
186 let index = EdgeIndex::forge(graph);
187 let router = crate::WalkRouter::forge(graph);
188 self.realize_indexed(name, graph, &index, &router, constraints, max_snap_m)
189 }
190
191 pub fn realize_indexed(
192 &self,
193 name: impl Into<String>,
194 graph: &WalkGraph,
195 index: &EdgeIndex,
196 router: &crate::WalkRouter,
197 constraints: &LoopConstraints,
198 max_snap_m: f64,
199 ) -> crate::Result<TrailRealization> {
200 self.validate()?;
201 if !max_snap_m.is_finite() || max_snap_m <= 0.0 {
202 return Err(TrailgenError::InvalidData(
203 "support-point snap distance must be positive".to_owned(),
204 ));
205 }
206 let anchors = bind_projections(graph, index, &self.support_points, max_snap_m)?;
207 let RoutedDesign {
208 spans,
209 support_span_offsets,
210 } = self.strike_spans(graph, router, &anchors)?;
211 let realized = materialize_walk(graph, &anchors, &spans, &support_span_offsets)?;
212 let start = realized.bindings[0].vertex;
213 let end = realized
214 .graph
215 .walk_edges(start, &realized.edges)
216 .ok_or_else(|| {
217 TrailgenError::InvalidData(
218 "support points induce an illegal directed trail".to_owned(),
219 )
220 })?;
221 let route = Route::from_edges(name, &realized.graph, start, realized.edges, constraints);
222 if !walk_fulfills_design(self.shape, route.metrics.shape, start == end) {
223 return Err(TrailgenError::ShapeMismatch {
224 actual: route.metrics.shape,
225 expected: self.shape,
226 });
227 }
228 Ok(TrailRealization {
229 trail: Self {
230 shape: self.shape,
231 support_points: realized
232 .bindings
233 .iter()
234 .map(|binding| binding.anchor)
235 .collect(),
236 routing: self.routing,
237 },
238 bindings: realized.bindings,
239 route,
240 walk: Arc::new(realized.graph),
241 support_offsets: realized.support_offsets,
242 source_spans: spans,
243 support_span_offsets,
244 })
245 }
246
247 fn strike_spans(
248 &self,
249 graph: &WalkGraph,
250 router: &crate::WalkRouter,
251 anchors: &[BoundProjection],
252 ) -> crate::Result<RoutedDesign> {
253 let mut workspace = router.workspace(graph);
254 let mut forge = PathForge {
255 router,
256 workspace: &mut workspace,
257 graph,
258 law: self.routing,
259 };
260 let mut spans = Vec::new();
261 let mut support_span_offsets = vec![0];
262 let mut previous = None;
263 for targets in anchors.windows(2) {
264 let leg = forge
265 .route(
266 targets[0].projection,
267 targets[1].projection,
268 previous,
269 PathVeto::default(),
270 )
271 .ok_or_else(|| {
272 TrailgenError::InvalidData(
273 "no lawful trail connects consecutive support points".to_owned(),
274 )
275 })?;
276 previous = leg.last().map(|span| span.edge).or(previous);
277 spans.extend(leg);
278 support_span_offsets.push(spans.len());
279 }
280 match self.shape {
281 RouteShape::Open => {}
282 RouteShape::OutAndBack => {
283 let mut reverse = spans
284 .iter()
285 .rev()
286 .map(|span| span.reversed())
287 .collect::<Vec<_>>();
288 spans.append(&mut reverse);
289 }
290 RouteShape::Loop => {
291 let forbidden_edges = spans.iter().map(|span| span.edge).collect::<BTreeSet<_>>();
292 let forbidden_vertices = walked_span_vertices(graph, &spans);
293 let return_path = forge
294 .route(
295 anchors.last().expect("validated anchors").projection,
296 anchors[0].projection,
297 previous,
298 PathVeto {
299 edges: Some(&forbidden_edges),
300 vertices: Some(&forbidden_vertices),
301 ..PathVeto::default()
302 },
303 )
304 .or_else(|| {
307 forge.route(
308 anchors.last().expect("validated anchors").projection,
309 anchors[0].projection,
310 previous,
311 PathVeto::default(),
312 )
313 })
314 .ok_or_else(|| {
315 TrailgenError::InvalidData(
316 "no lawful return connects the final support point".to_owned(),
317 )
318 })?;
319 spans.extend(return_path);
320 }
321 RouteShape::FigureEight => unreachable!("figure-eight rejected by validation"),
322 }
323 Ok(RoutedDesign {
324 spans,
325 support_span_offsets,
326 })
327 }
328}
329
330struct RoutedDesign {
331 spans: Vec<PartialSpan>,
332 support_span_offsets: Vec<usize>,
333}
334
335fn walk_fulfills_design(design: RouteShape, morphology: RouteShape, closed: bool) -> bool {
340 match design {
341 RouteShape::Loop => closed,
342 RouteShape::Open | RouteShape::OutAndBack => morphology == design,
343 RouteShape::FigureEight => false,
344 }
345}
346
347fn route_vertices(graph: &WalkGraph, route: &Route) -> Option<Vec<VertexId>> {
348 let mut vertices = Vec::with_capacity(route.edges.len() + 1);
349 let mut at = route.start;
350 let mut previous = None;
351 vertices.push(at);
352 for edge in &route.edges {
353 let segment = graph.edges.get(edge.0)?;
354 if !graph.turn_allowed(previous, at, *edge) {
355 return None;
356 }
357 at = segment.traverse(at)?;
358 previous = Some(*edge);
359 vertices.push(at);
360 }
361 Some(vertices)
362}
363
364fn compress_arc(
365 graph: &WalkGraph,
366 route: &Route,
367 vertices: &[VertexId],
368 routing: RoutingLaw,
369 lo: usize,
370 hi: usize,
371 supports: &mut Vec<SupportPoint>,
372) -> Option<()> {
373 let previous = lo.checked_sub(1).map(|index| route.edges[index]);
374 if shortest_path(graph, vertices[lo], vertices[hi], previous, routing, None)
375 .is_some_and(|shortest| shortest == route.edges[lo..hi])
376 {
377 supports.push(vertex_support(graph, vertices[lo])?);
378 return Some(());
379 }
380 if hi - lo <= 1 {
381 supports.push(vertex_support(graph, vertices[lo])?);
382 let edge = &graph.edges[route.edges[lo].0];
383 supports.push(SupportPoint::forge(line_coord_at(
384 &edge.geometry,
385 edge.geometry.length_m() * 0.5,
386 ))?);
387 return Some(());
388 }
389 let split = lo + (hi - lo) / 2;
390 compress_arc(graph, route, vertices, routing, lo, split, supports)?;
391 compress_arc(graph, route, vertices, routing, split, hi, supports)
392}
393
394fn compress_span_arc(
395 forge: &mut PathForge<'_>,
396 spans: &[PartialSpan],
397 lo: usize,
398 hi: usize,
399 supports: &mut Vec<SupportPoint>,
400) -> Option<()> {
401 let start = span_projection(forge.graph, spans[lo], true);
402 let target = span_projection(forge.graph, spans[hi - 1], false);
403 let previous = lo.checked_sub(1).map(|slot| spans[slot].edge);
404 if forge
405 .route(start, target, previous, PathVeto::default())
406 .is_some_and(|shortest| same_spans(&shortest, &spans[lo..hi]))
407 {
408 supports.push(SupportPoint::forge(start.coord)?);
409 return Some(());
410 }
411 if hi - lo <= 1 {
412 supports.push(SupportPoint::forge(start.coord)?);
413 let span = spans[lo];
414 supports.push(SupportPoint::forge(line_coord_at(
415 &forge.graph.edges[span.edge.0].geometry,
416 (span.from_m + span.to_m) * 0.5,
417 ))?);
418 return Some(());
419 }
420 let split = lo + (hi - lo) / 2;
421 compress_span_arc(forge, spans, lo, split, supports)?;
422 compress_span_arc(forge, spans, split, hi, supports)
423}
424
425fn span_projection(graph: &WalkGraph, span: PartialSpan, start: bool) -> EdgeProjection {
426 let progress_m = if start { span.from_m } else { span.to_m };
427 EdgeProjection {
428 edge: span.edge,
429 coord: line_coord_at(&graph.edges[span.edge.0].geometry, progress_m),
430 progress_m,
431 distance_m: 0.0,
432 }
433}
434
435fn same_spans(left: &[PartialSpan], right: &[PartialSpan]) -> bool {
436 left.len() == right.len()
437 && left.iter().zip(right).all(|(left, right)| {
438 left.edge == right.edge
439 && (left.from_m - right.from_m).abs() <= SUPPORT_EPSILON_M
440 && (left.to_m - right.to_m).abs() <= SUPPORT_EPSILON_M
441 })
442}
443
444fn vertex_support(graph: &WalkGraph, vertex: VertexId) -> Option<SupportPoint> {
445 SupportPoint::forge(graph.vertices[vertex.0].coord)
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
449#[serde(deny_unknown_fields)]
450pub struct SupportBinding {
451 pub requested: SupportPoint,
452 pub anchor: SupportPoint,
453 pub vertex: VertexId,
454 pub snap_m: f64,
455}
456
457#[derive(Clone, Debug, PartialEq)]
458pub struct TrailRealization {
459 pub trail: Trail,
460 pub bindings: Vec<SupportBinding>,
461 pub route: Route,
462 walk: Arc<WalkGraph>,
463 support_offsets: Vec<usize>,
464 source_spans: Vec<PartialSpan>,
465 support_span_offsets: Vec<usize>,
466}
467
468#[derive(Clone, Debug, PartialEq)]
469pub struct TrailReversal {
470 pub trail: Trail,
471 pub added_supports: usize,
472}
473
474#[derive(Clone, Copy, Debug, PartialEq)]
475pub struct SupportInsertion {
476 pub slot: usize,
477 pub distance_m: f64,
478}
479
480impl TrailRealization {
481 #[must_use]
482 pub fn graph(&self) -> &WalkGraph {
483 &self.walk
484 }
485
486 #[must_use]
490 pub fn walk_fingerprint(&self) -> u64 {
491 self.source_spans.iter().fold(
492 (self.source_spans.len() as u64) ^ 0xa076_1d64_78bd_642f,
493 |state, span| {
494 [
495 span.edge.0 as u64,
496 span.from_m.to_bits(),
497 span.to_m.to_bits(),
498 ]
499 .into_iter()
500 .fold(state, |state, word| {
501 (state ^ word.wrapping_mul(0xe703_7ed1_a0b4_28db))
502 .rotate_left(23)
503 .wrapping_mul(0x8ebc_6af0_9c88_c6e3)
504 })
505 },
506 )
507 }
508
509 pub fn reverse_loop(&self, source: &WalkGraph) -> crate::Result<TrailReversal> {
513 if self.trail.shape != RouteShape::Loop {
514 return Err(TrailgenError::ShapeMismatch {
515 actual: self.trail.shape,
516 expected: RouteShape::Loop,
517 });
518 }
519 if self.source_spans.iter().any(|span| {
520 source
521 .edges
522 .get(span.edge.0)
523 .is_none_or(|edge| edge.attr.travel != crate::EdgeTravel::Both)
524 }) {
525 return Err(TrailgenError::OneWayReversal);
526 }
527
528 let reversed = self
529 .source_spans
530 .iter()
531 .rev()
532 .map(|span| span.reversed())
533 .collect::<Vec<_>>();
534 let span_count = reversed.len();
535 debug_assert_eq!(
536 self.support_span_offsets.len(),
537 self.trail.support_points.len()
538 );
539
540 let fixed = std::iter::once((0, self.trail.support_points[0]))
541 .chain((1..self.trail.support_points.len()).rev().map(|slot| {
542 (
543 span_count - self.support_span_offsets[slot],
544 self.trail.support_points[slot],
545 )
546 }))
547 .collect::<Vec<_>>();
548 let mut support_points = Vec::with_capacity(fixed.len());
549 support_points.push(fixed[0].1);
550 let mut added_supports = 0;
551 let router = crate::WalkRouter::forge(source);
552 let mut workspace = router.workspace(source);
553 let mut forge = PathForge {
554 router: &router,
555 workspace: &mut workspace,
556 graph: source,
557 law: self.trail.routing,
558 };
559 for slot in 0..fixed.len() {
560 let lo = fixed[slot].0;
561 let hi = fixed.get(slot + 1).map_or(span_count, |next| next.0);
562 if lo < hi {
563 let mut repaired = Vec::new();
564 compress_span_arc(&mut forge, &reversed, lo, hi, &mut repaired)
565 .ok_or(TrailgenError::UnrepresentableReversal)?;
566 debug_assert_eq!(repaired.first(), Some(&fixed[slot].1));
567 added_supports += repaired.len() - 1;
568 support_points.extend(repaired.into_iter().skip(1));
569 }
570 if let Some(next) = fixed.get(slot + 1) {
571 support_points.push(next.1);
572 }
573 }
574
575 Ok(TrailReversal {
576 trail: Trail::forge(RouteShape::Loop, support_points, self.trail.routing)?,
577 added_supports,
578 })
579 }
580
581 #[must_use]
585 pub fn support_insertion(&self, requested: Coord) -> Option<SupportInsertion> {
586 let graph = self.graph();
587 let routed = if self.trail.shape == RouteShape::OutAndBack {
588 *self.support_offsets.last()?
589 } else {
590 self.route.edges.len()
591 };
592 let (edge_slot, distance_m) = self
593 .route
594 .edges
595 .iter()
596 .take(routed)
597 .enumerate()
598 .filter_map(|(slot, edge)| {
599 crate::model::line_projection(&graph.edges[edge.0].geometry, requested)
600 .map(|(distance_m, _, _)| (slot, distance_m))
601 })
602 .min_by(|left, right| left.1.total_cmp(&right.1))?;
603 let slot = self.support_offsets[1..].partition_point(|offset| *offset <= edge_slot) + 1;
604 Some(SupportInsertion { slot, distance_m })
605 }
606}
607
608const SUPPORT_EPSILON_M: f64 = 0.05;
609
610#[derive(Clone, Copy)]
611struct BoundProjection {
612 requested: SupportPoint,
613 projection: EdgeProjection,
614}
615
616#[derive(Clone, Copy, Debug, PartialEq)]
617struct PartialSpan {
618 edge: EdgeId,
619 from_m: f64,
620 to_m: f64,
621}
622
623impl PartialSpan {
624 const fn reversed(self) -> Self {
625 Self {
626 edge: self.edge,
627 from_m: self.to_m,
628 to_m: self.from_m,
629 }
630 }
631}
632
633fn bind_projections(
634 graph: &WalkGraph,
635 index: &EdgeIndex,
636 supports: &[SupportPoint],
637 max_snap_m: f64,
638) -> crate::Result<Vec<BoundProjection>> {
639 supports
640 .iter()
641 .copied()
642 .map(|requested| {
643 let projection = index.project(graph, requested.coord()).ok_or_else(|| {
644 TrailgenError::InvalidData(
645 "cannot bind a support point to an empty network".to_owned(),
646 )
647 })?;
648 if projection.distance_m > max_snap_m {
649 return Err(TrailgenError::InvalidData(format!(
650 "support point lies {:.0} m from the walking network",
651 projection.distance_m
652 )));
653 }
654 Ok(BoundProjection {
655 requested,
656 projection,
657 })
658 })
659 .collect()
660}
661
662#[derive(Clone, Copy)]
663struct EndpointRoute {
664 vertex: VertexId,
665 span: Option<PartialSpan>,
666 cost: f64,
667 previous: Option<EdgeId>,
668}
669
670struct PathForge<'a> {
671 router: &'a crate::WalkRouter,
672 workspace: &'a mut crate::RoutingWorkspace,
673 graph: &'a WalkGraph,
674 law: RoutingLaw,
675}
676
677impl PathForge<'_> {
678 fn route(
679 &mut self,
680 from: EdgeProjection,
681 target: EdgeProjection,
682 previous: Option<EdgeId>,
683 veto: PathVeto<'_>,
684 ) -> Option<Vec<PartialSpan>> {
685 let mut alternatives = Vec::<(f64, Vec<PartialSpan>)>::new();
686 if from.edge == target.edge
687 && span_allowed(self.graph, from.edge, from.progress_m, target.progress_m)
688 && veto.edges.is_none_or(|edges| !edges.contains(&from.edge))
689 {
690 let edge = &self.graph.edges[from.edge.0];
691 let at_endpoint = endpoint_at(edge, from.progress_m);
692 if at_endpoint.is_none_or(|via| self.graph.turn_allowed(previous, via, from.edge)) {
693 let span = PartialSpan {
694 edge: from.edge,
695 from_m: from.progress_m,
696 to_m: target.progress_m,
697 };
698 if let Some(cost) = partial_cost(self.graph, self.law, span)
699 && veto.cost_ceiling.is_none_or(|ceiling| cost <= ceiling)
700 {
701 alternatives.push((cost, vec![span]));
702 }
703 }
704 }
705
706 for departure in departure_routes(self.graph, from, previous, self.law) {
707 if departure
708 .span
709 .is_some_and(|span| veto.edges.is_some_and(|edges| edges.contains(&span.edge)))
710 {
711 continue;
712 }
713 for arrival in arrival_routes(self.graph, target, self.law) {
714 if arrival
715 .span
716 .is_some_and(|span| veto.edges.is_some_and(|edges| edges.contains(&span.edge)))
717 {
718 continue;
719 }
720 let fixed_cost = departure.cost + arrival.cost;
721 let ceiling = veto.cost_ceiling.map(|ceiling| ceiling - fixed_cost);
722 if ceiling.is_some_and(|ceiling| ceiling < 0.0) {
723 continue;
724 }
725 let Some(path) = self.router.shortest_path(
726 self.graph,
727 self.workspace,
728 crate::RouteRequest {
729 from: departure.vertex,
730 target: arrival.vertex,
731 previous: departure.previous,
732 law: self.law,
733 cost_ceiling: ceiling,
734 forbidden_edges: veto.edges,
735 forbidden_vertices: veto.vertices,
736 },
737 ) else {
738 continue;
739 };
740 if let Some(span) = arrival.span
741 && !self.graph.turn_allowed(
742 path.last().copied().or(departure.previous),
743 arrival.vertex,
744 span.edge,
745 )
746 {
747 continue;
748 }
749 let mut spans = Vec::with_capacity(path.len() + 2);
750 if let Some(prefix) = departure.span {
751 spans.push(prefix);
752 }
753 append_full_spans(self.graph, departure.vertex, &path, &mut spans)?;
754 if let Some(suffix) = arrival.span {
755 spans.push(suffix);
756 }
757 let cost = fixed_cost
758 + path
759 .iter()
760 .map(|edge| self.law.edge_cost(self.graph, *edge))
761 .sum::<Option<f64>>()?;
762 alternatives.push((cost, spans));
763 }
764 }
765 alternatives
766 .into_iter()
767 .min_by(|left, right| {
768 left.0
769 .total_cmp(&right.0)
770 .then_with(|| span_key(&left.1).cmp(&span_key(&right.1)))
771 })
772 .map(|(_, spans)| spans)
773 }
774}
775
776fn departure_routes(
777 graph: &WalkGraph,
778 projection: EdgeProjection,
779 previous: Option<EdgeId>,
780 law: RoutingLaw,
781) -> Vec<EndpointRoute> {
782 let edge = &graph.edges[projection.edge.0];
783 if let Some(vertex) = endpoint_at(edge, projection.progress_m) {
784 return vec![EndpointRoute {
785 vertex,
786 span: None,
787 cost: 0.0,
788 previous,
789 }];
790 }
791 [(edge.a, 0.0), (edge.b, edge.geometry.length_m())]
792 .into_iter()
793 .filter_map(|(vertex, to_m)| {
794 let span = PartialSpan {
795 edge: projection.edge,
796 from_m: projection.progress_m,
797 to_m,
798 };
799 if !span_allowed(graph, span.edge, span.from_m, span.to_m) {
800 return None;
801 }
802 Some(EndpointRoute {
803 vertex,
804 span: Some(span),
805 cost: partial_cost(graph, law, span)?,
806 previous: Some(span.edge),
807 })
808 })
809 .collect()
810}
811
812fn arrival_routes(
813 graph: &WalkGraph,
814 projection: EdgeProjection,
815 law: RoutingLaw,
816) -> Vec<EndpointRoute> {
817 let edge = &graph.edges[projection.edge.0];
818 if let Some(vertex) = endpoint_at(edge, projection.progress_m) {
819 return vec![EndpointRoute {
820 vertex,
821 span: None,
822 cost: 0.0,
823 previous: None,
824 }];
825 }
826 [(edge.a, 0.0), (edge.b, edge.geometry.length_m())]
827 .into_iter()
828 .filter_map(|(vertex, from_m)| {
829 let span = PartialSpan {
830 edge: projection.edge,
831 from_m,
832 to_m: projection.progress_m,
833 };
834 if !span_allowed(graph, span.edge, span.from_m, span.to_m) {
835 return None;
836 }
837 Some(EndpointRoute {
838 vertex,
839 span: Some(span),
840 cost: partial_cost(graph, law, span)?,
841 previous: None,
842 })
843 })
844 .collect()
845}
846
847fn span_allowed(graph: &WalkGraph, edge: EdgeId, from_m: f64, to_m: f64) -> bool {
848 use crate::EdgeTravel;
849 match graph.edges[edge.0].attr.travel {
850 EdgeTravel::Both => true,
851 EdgeTravel::Forward => to_m >= from_m,
852 EdgeTravel::Backward => to_m <= from_m,
853 }
854}
855
856fn partial_cost(graph: &WalkGraph, law: RoutingLaw, span: PartialSpan) -> Option<f64> {
857 let full_m = graph.edges[span.edge.0].geometry.length_m();
858 let ratio = (span.to_m - span.from_m).abs() / full_m.max(f64::EPSILON);
859 Some(law.edge_cost(graph, span.edge)? * ratio)
860}
861
862fn endpoint_at(edge: &Edge, progress_m: f64) -> Option<VertexId> {
863 if progress_m <= SUPPORT_EPSILON_M {
864 Some(edge.a)
865 } else if edge.geometry.length_m() - progress_m <= SUPPORT_EPSILON_M {
866 Some(edge.b)
867 } else {
868 None
869 }
870}
871
872fn append_full_spans(
873 graph: &WalkGraph,
874 mut at: VertexId,
875 path: &[EdgeId],
876 spans: &mut Vec<PartialSpan>,
877) -> Option<()> {
878 for edge_id in path {
879 let edge = &graph.edges[edge_id.0];
880 let to = edge.traverse(at)?;
881 spans.push(PartialSpan {
882 edge: *edge_id,
883 from_m: if at == edge.a {
884 0.0
885 } else {
886 edge.geometry.length_m()
887 },
888 to_m: if to == edge.b {
889 edge.geometry.length_m()
890 } else {
891 0.0
892 },
893 });
894 at = to;
895 }
896 Some(())
897}
898
899fn span_key(spans: &[PartialSpan]) -> Vec<(EdgeId, u64, u64)> {
900 spans
901 .iter()
902 .map(|span| (span.edge, span.from_m.to_bits(), span.to_m.to_bits()))
903 .collect()
904}
905
906fn walked_span_vertices(graph: &WalkGraph, spans: &[PartialSpan]) -> BTreeSet<VertexId> {
907 spans
908 .iter()
909 .flat_map(|span| {
910 let edge = &graph.edges[span.edge.0];
911 [endpoint_at(edge, span.from_m), endpoint_at(edge, span.to_m)]
912 })
913 .flatten()
914 .collect()
915}
916
917struct MaterializedWalk {
918 graph: WalkGraph,
919 bindings: Vec<SupportBinding>,
920 edges: Vec<EdgeId>,
921 support_offsets: Vec<usize>,
922}
923
924#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
925enum LocalVertex {
926 Base(VertexId),
927 Interior(EdgeId, u64),
928}
929
930fn materialize_walk(
931 source: &WalkGraph,
932 anchors: &[BoundProjection],
933 spans: &[PartialSpan],
934 support_span_offsets: &[usize],
935) -> crate::Result<MaterializedWalk> {
936 let mut cuts = BTreeMap::<EdgeId, Vec<f64>>::new();
937 for span in spans {
938 cuts.entry(span.edge)
939 .or_default()
940 .extend([span.from_m, span.to_m]);
941 }
942 for anchor in anchors {
943 cuts.entry(anchor.projection.edge)
944 .or_default()
945 .push(anchor.projection.progress_m);
946 }
947 for (edge, marks) in &mut cuts {
948 let length_m = source.edges[edge.0].geometry.length_m();
949 marks.extend([0.0, length_m]);
950 marks.sort_by(f64::total_cmp);
951 marks.dedup_by(|left, right| (*left - *right).abs() <= SUPPORT_EPSILON_M);
952 }
953
954 let mut vertices = Vec::new();
955 let mut vertex_ids = BTreeMap::<LocalVertex, VertexId>::new();
956 let mut edges = Vec::new();
957 let mut edge_ids = BTreeMap::<(EdgeId, u64, u64), EdgeId>::new();
958 let mut lineage = Vec::new();
959 let mut route = Vec::new();
960 let mut span_offsets = Vec::with_capacity(spans.len() + 1);
961 span_offsets.push(0);
962 for span in spans {
963 let marks = &cuts[&span.edge];
964 let from = canonical_mark(marks, span.from_m);
965 let to = canonical_mark(marks, span.to_m);
966 let lo = from.min(to);
967 let hi = from.max(to);
968 let mut intervals = marks
969 .windows(2)
970 .filter(|pair| pair[0] >= lo - SUPPORT_EPSILON_M && pair[1] <= hi + SUPPORT_EPSILON_M)
971 .map(|pair| (pair[0], pair[1]))
972 .collect::<Vec<_>>();
973 if to < from {
974 intervals.reverse();
975 }
976 for (lo, hi) in intervals {
977 let edge = materialized_edge(
978 source,
979 span.edge,
980 lo,
981 hi,
982 &mut vertices,
983 &mut vertex_ids,
984 &mut edges,
985 &mut edge_ids,
986 &mut lineage,
987 );
988 route.push(edge);
989 }
990 span_offsets.push(route.len());
991 }
992
993 let bindings = anchors
994 .iter()
995 .map(|bound| {
996 let marks = &cuts[&bound.projection.edge];
997 let progress_m = canonical_mark(marks, bound.projection.progress_m);
998 let vertex = materialized_vertex(
999 source,
1000 bound.projection.edge,
1001 progress_m,
1002 &mut vertices,
1003 &mut vertex_ids,
1004 );
1005 let anchor = SupportPoint(line_coord_at(
1006 &source.edges[bound.projection.edge.0].geometry,
1007 progress_m,
1008 ));
1009 SupportBinding {
1010 requested: bound.requested,
1011 anchor,
1012 vertex,
1013 snap_m: bound.requested.coord().haversine_m(anchor.coord()),
1014 }
1015 })
1016 .collect::<Vec<_>>();
1017 let support_offsets = support_span_offsets
1018 .iter()
1019 .map(|offset| span_offsets[*offset])
1020 .collect();
1021 let mut graph = WalkGraph::new(vertices, edges);
1022 graph.turn_bans = inherited_turn_bans(source, &graph, &lineage);
1023 graph.validate()?;
1024 Ok(MaterializedWalk {
1025 graph,
1026 bindings,
1027 edges: route,
1028 support_offsets,
1029 })
1030}
1031
1032#[allow(clippy::too_many_arguments)]
1033fn materialized_edge(
1034 source: &WalkGraph,
1035 source_edge: EdgeId,
1036 lo: f64,
1037 hi: f64,
1038 vertices: &mut Vec<Vertex>,
1039 vertex_ids: &mut BTreeMap<LocalVertex, VertexId>,
1040 edges: &mut Vec<Edge>,
1041 edge_ids: &mut BTreeMap<(EdgeId, u64, u64), EdgeId>,
1042 lineage: &mut Vec<EdgeId>,
1043) -> EdgeId {
1044 let key = (source_edge, lo.to_bits(), hi.to_bits());
1045 if let Some(edge) = edge_ids.get(&key) {
1046 return *edge;
1047 }
1048 let a = materialized_vertex(source, source_edge, lo, vertices, vertex_ids);
1049 let b = materialized_vertex(source, source_edge, hi, vertices, vertex_ids);
1050 let original = &source.edges[source_edge.0];
1051 let span_m = original.geometry.length_m();
1052 let geometry = line_slice(&original.geometry, lo, hi);
1053 let carries_crossings = lo <= span_m * 0.5 && span_m * 0.5 < hi;
1054 let id = EdgeId(edges.len());
1055 edges.push(Edge {
1056 id,
1057 a,
1058 b,
1059 attr: cleaved_attr(original, &geometry, span_m, carries_crossings),
1060 geometry,
1061 });
1062 lineage.push(source_edge);
1063 edge_ids.insert(key, id);
1064 id
1065}
1066
1067fn materialized_vertex(
1068 source: &WalkGraph,
1069 source_edge: EdgeId,
1070 progress_m: f64,
1071 vertices: &mut Vec<Vertex>,
1072 vertex_ids: &mut BTreeMap<LocalVertex, VertexId>,
1073) -> VertexId {
1074 let edge = &source.edges[source_edge.0];
1075 let key = if progress_m <= SUPPORT_EPSILON_M {
1076 LocalVertex::Base(edge.a)
1077 } else if edge.geometry.length_m() - progress_m <= SUPPORT_EPSILON_M {
1078 LocalVertex::Base(edge.b)
1079 } else {
1080 LocalVertex::Interior(source_edge, progress_m.to_bits())
1081 };
1082 if let Some(vertex) = vertex_ids.get(&key) {
1083 return *vertex;
1084 }
1085 let id = VertexId(vertices.len());
1086 let (coord, junction) = match key {
1087 LocalVertex::Base(base) => {
1088 let base = &source.vertices[base.0];
1089 (base.coord, base.junction.clone())
1090 }
1091 LocalVertex::Interior(_, _) => (line_coord_at(&edge.geometry, progress_m), None),
1092 };
1093 vertices.push(Vertex {
1094 id,
1095 coord,
1096 junction,
1097 });
1098 vertex_ids.insert(key, id);
1099 id
1100}
1101
1102fn canonical_mark(marks: &[f64], progress_m: f64) -> f64 {
1103 marks
1104 .iter()
1105 .copied()
1106 .find(|mark| (*mark - progress_m).abs() <= SUPPORT_EPSILON_M)
1107 .unwrap_or(progress_m)
1108}
1109
1110fn inherited_turn_bans(
1111 source: &WalkGraph,
1112 realized: &WalkGraph,
1113 lineage: &[EdgeId],
1114) -> Vec<TurnBan> {
1115 source
1116 .turn_bans
1117 .iter()
1118 .filter_map(|ban| {
1119 let via = realized
1120 .vertices
1121 .iter()
1122 .find(|vertex| {
1123 vertex.coord == source.vertices[ban.via.0].coord
1124 && vertex.junction == source.vertices[ban.via.0].junction
1125 })?
1126 .id;
1127 let incident = |source_edge: EdgeId| {
1128 lineage.iter().enumerate().find_map(|(slot, lineage)| {
1129 (*lineage == source_edge
1130 && [realized.edges[slot].a, realized.edges[slot].b].contains(&via))
1131 .then_some(EdgeId(slot))
1132 })
1133 };
1134 Some(TurnBan {
1135 via,
1136 from: incident(ban.from)?,
1137 to: incident(ban.to)?,
1138 provenance: ban.provenance.clone(),
1139 })
1140 })
1141 .collect()
1142}
1143
1144fn line_coord_at(line: &LineString, progress_m: f64) -> Coord {
1145 let mut traversed_m = 0.0;
1146 for segment in line.points.windows(2) {
1147 let length_m = segment[0].haversine_m(segment[1]);
1148 if progress_m <= traversed_m + length_m {
1149 let t = if length_m <= f64::EPSILON {
1150 0.0
1151 } else {
1152 (progress_m - traversed_m) / length_m
1153 };
1154 return segment[0].lerp(segment[1], t.clamp(0.0, 1.0));
1155 }
1156 traversed_m += length_m;
1157 }
1158 line.end()
1159}
1160
1161fn line_slice(line: &LineString, start_m: f64, end_m: f64) -> LineString {
1162 let mut points = vec![line_coord_at(line, start_m)];
1163 let mut traversed_m = 0.0;
1164 for segment in line.points.windows(2) {
1165 traversed_m += segment[0].haversine_m(segment[1]);
1166 if traversed_m > start_m && traversed_m < end_m {
1167 points.push(segment[1]);
1168 }
1169 }
1170 let end = line_coord_at(line, end_m);
1171 if points.last() != Some(&end) {
1172 points.push(end);
1173 }
1174 if points.len() == 1 {
1175 points.push(end);
1176 }
1177 LineString::unchecked(points)
1178}
1179
1180fn cleaved_attr(
1181 source: &Edge,
1182 geometry: &LineString,
1183 span_m: f64,
1184 carries_crossings: bool,
1185) -> EdgeAttr {
1186 let ratio = geometry.length_m() / span_m;
1187 let (measured_ascent, measured_descent) = geometry.ascent_descent_m();
1188 let measured = geometry.points.iter().all(|point| point.ele.is_some());
1189 let ascent_m = if measured {
1190 measured_ascent
1191 } else {
1192 source.attr.ascent_m * ratio
1193 };
1194 let descent_m = if measured {
1195 measured_descent
1196 } else {
1197 source.attr.descent_m * ratio
1198 };
1199 let mut attr = source.attr.clone();
1200 attr.length_m *= ratio;
1201 attr.ascent_m = ascent_m;
1202 attr.descent_m = descent_m;
1203 attr.sustained_steep_m *= ratio;
1204 attr.grade_distribution = scale_grades(attr.grade_distribution, ratio);
1205 attr.traversal = HikingModel.estimate(geometry, &attr);
1206 if !carries_crossings {
1207 attr.crossings.clear();
1208 }
1209 attr
1210}
1211
1212const fn scale_grades(grades: GradeDistribution, ratio: f64) -> GradeDistribution {
1213 GradeDistribution {
1214 flat_m: grades.flat_m * ratio,
1215 rolling_m: grades.rolling_m * ratio,
1216 steep_m: grades.steep_m * ratio,
1217 savage_m: grades.savage_m * ratio,
1218 }
1219}
1220
1221#[derive(Clone, Copy, Default)]
1222struct PathVeto<'a> {
1223 cost_ceiling: Option<f64>,
1224 edges: Option<&'a BTreeSet<EdgeId>>,
1225 vertices: Option<&'a BTreeSet<VertexId>>,
1226}
1227
1228pub(crate) fn shortest_path(
1229 graph: &WalkGraph,
1230 from: VertexId,
1231 target: VertexId,
1232 previous: Option<EdgeId>,
1233 law: RoutingLaw,
1234 max_cost: Option<f64>,
1235) -> Option<Vec<EdgeId>> {
1236 shortest_path_excluding(
1237 graph,
1238 from,
1239 target,
1240 previous,
1241 law,
1242 PathVeto {
1243 cost_ceiling: max_cost,
1244 ..PathVeto::default()
1245 },
1246 )
1247}
1248
1249fn shortest_path_excluding(
1250 graph: &WalkGraph,
1251 from: VertexId,
1252 target: VertexId,
1253 previous: Option<EdgeId>,
1254 law: RoutingLaw,
1255 veto: PathVeto<'_>,
1256) -> Option<Vec<EdgeId>> {
1257 let router = crate::WalkRouter::forge(graph);
1258 let mut workspace = router.workspace(graph);
1259 route_path_excluding(
1260 &router,
1261 &mut workspace,
1262 graph,
1263 from,
1264 target,
1265 previous,
1266 law,
1267 veto.edges,
1268 veto.vertices,
1269 veto.cost_ceiling,
1270 )
1271}
1272
1273#[allow(clippy::too_many_arguments)]
1274fn route_path_excluding(
1275 router: &crate::WalkRouter,
1276 workspace: &mut crate::RoutingWorkspace,
1277 graph: &WalkGraph,
1278 from: VertexId,
1279 target: VertexId,
1280 previous: Option<EdgeId>,
1281 law: RoutingLaw,
1282 forbidden_edges: Option<&BTreeSet<EdgeId>>,
1283 forbidden_vertices: Option<&BTreeSet<VertexId>>,
1284 cost_ceiling: Option<f64>,
1285) -> Option<Vec<EdgeId>> {
1286 router.shortest_path(
1287 graph,
1288 workspace,
1289 crate::RouteRequest {
1290 from,
1291 target,
1292 previous,
1293 law,
1294 cost_ceiling,
1295 forbidden_edges,
1296 forbidden_vertices,
1297 },
1298 )
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304 use crate::{
1305 CrossingControl, EdgeTravel, GeometryClaim, GraphBuilder, JunctionPolicy, LineString,
1306 Provenance, SegmentDraft, TrailStanding, WayKind, WayRealm, io::geojson,
1307 };
1308
1309 fn graph() -> WalkGraph {
1310 GraphBuilder::default()
1311 .build(
1312 &geojson::network_from_str(include_str!("../tests/fixtures/mini_network.geojson"))
1313 .expect("parse fixture"),
1314 )
1315 .expect("build fixture")
1316 }
1317
1318 fn draft(name: &str, from: Coord, to: Coord) -> SegmentDraft {
1319 SegmentDraft {
1320 geometry: LineString::new(vec![from, to]).expect("valid line"),
1321 junctions: JunctionPolicy::Planar,
1322 turn_ref: None,
1323 junction_keys: None,
1324 turn_restrictions: Vec::new(),
1325 way_kind: WayKind::Path,
1326 realm: WayRealm::default(),
1327 geometry_claim: GeometryClaim::default(),
1328 crossing_control: CrossingControl::default(),
1329 standing: TrailStanding::Established,
1330 marking: crate::TrailMarking::default(),
1331 terrain: Terrain::Trail,
1332 terrain_confidence: Some(1.0),
1333 surface: Some("dirt".to_owned()),
1334 access: Access::Open,
1335 travel: EdgeTravel::Both,
1336 road_exposure: 0.0,
1337 confidence: 1.0,
1338 provenance: vec![Provenance::fixture(name)],
1339 }
1340 }
1341
1342 fn vertex_at(graph: &WalkGraph, coord: Coord) -> VertexId {
1343 graph
1344 .vertices
1345 .iter()
1346 .find(|vertex| vertex.coord.planar_distance2(coord) < 1.0e-16)
1347 .expect("coordinate vertex")
1348 .id
1349 }
1350
1351 fn edge_between(graph: &WalkGraph, from: VertexId, to: VertexId) -> EdgeId {
1352 graph.adjacency[from.0]
1353 .iter()
1354 .copied()
1355 .find(|edge| graph.edges[edge.0].other(from) == Some(to))
1356 .expect("adjacent vertices")
1357 }
1358
1359 fn loop_constraints() -> LoopConstraints {
1360 LoopConstraints {
1361 min_distance_m: 0.0,
1362 max_distance_m: f64::MAX,
1363 max_repeated_edge_fraction: 0.0,
1364 allowed_shapes: vec![RouteShape::Loop],
1365 ..LoopConstraints::default()
1366 }
1367 }
1368
1369 #[test]
1370 fn out_and_back_may_turn_inside_an_edge() {
1371 let start_coord = Coord::with_ele(0.0, 0.0, 0.0);
1372 let turn_coord = Coord::new(0.001, 0.000_004);
1373 let graph = GraphBuilder::default()
1374 .build(&[SegmentDraft {
1375 geometry: LineString::new(vec![start_coord, Coord::with_ele(0.004, 0.0, 40.0)])
1376 .expect("valid line"),
1377 junctions: JunctionPolicy::Planar,
1378 turn_ref: None,
1379 junction_keys: None,
1380 turn_restrictions: Vec::new(),
1381 way_kind: WayKind::Path,
1382 realm: WayRealm::default(),
1383 geometry_claim: GeometryClaim::default(),
1384 crossing_control: CrossingControl::default(),
1385 standing: TrailStanding::Established,
1386 marking: crate::TrailMarking::default(),
1387 terrain: Terrain::Trail,
1388 terrain_confidence: Some(1.0),
1389 surface: Some("dirt".to_owned()),
1390 access: Access::Open,
1391 travel: EdgeTravel::Both,
1392 road_exposure: 0.0,
1393 confidence: 1.0,
1394 provenance: vec![Provenance::fixture("long-edge")],
1395 }])
1396 .expect("build line");
1397 let source_length_m = graph.edges[0].attr.length_m;
1398 let trail = Trail::forge(
1399 RouteShape::OutAndBack,
1400 vec![
1401 SupportPoint::forge(start_coord).expect("valid start"),
1402 SupportPoint::forge(turn_coord).expect("valid turn"),
1403 ],
1404 RoutingLaw::default(),
1405 )
1406 .expect("valid trail");
1407 let constraints = LoopConstraints {
1408 min_distance_m: 0.0,
1409 max_distance_m: f64::MAX,
1410 max_repeated_edge_fraction: 1.0,
1411 allowed_shapes: vec![RouteShape::OutAndBack],
1412 ..LoopConstraints::default()
1413 };
1414
1415 let realized = trail
1416 .realize("partial", &graph, &constraints, 2.0)
1417 .expect("realize partial edge");
1418 let local = realized.graph();
1419
1420 assert_eq!(graph.vertices.len(), 2, "the corpus remains immutable");
1421 assert_eq!(graph.edges.len(), 1, "the corpus remains immutable");
1422 assert_eq!(local.vertices.len(), 2, "the realized graph is route-local");
1423 assert_eq!(
1424 local.edges.len(),
1425 1,
1426 "only the walked half-edge is materialized"
1427 );
1428 assert_eq!(realized.route.edges.len(), 2);
1429 assert_eq!(realized.route.edges[0], realized.route.edges[1]);
1430 assert!((realized.route.metrics.distance_m - source_length_m / 2.0).abs() < 0.5);
1431 assert!((realized.route.metrics.ascent_m - 10.0).abs() < 0.1);
1432 assert!((realized.route.metrics.descent_m - 10.0).abs() < 0.1);
1433 let anchor = realized.trail.support_points[1].coord();
1434 assert!(anchor.lat.abs() < 1.0e-10);
1435 assert!((anchor.lon - 0.001).abs() < 1.0e-8);
1436 assert_eq!(local.vertices[realized.bindings[1].vertex.0].coord, anchor);
1437 }
1438
1439 #[test]
1440 fn loop_candidates_recover_exact_support_designs() {
1441 let graph = graph();
1442 let constraints = loop_constraints();
1443 let route = crate::ExactLoopSolver::default()
1444 .enumerate(&graph, VertexId(0), &constraints, 1)
1445 .into_iter()
1446 .next()
1447 .expect("fixture has a loop");
1448 let trail = Trail::infer(&graph, &route, RoutingLaw::default())
1449 .expect("loop has an exact support design");
1450 let realized = trail
1451 .realize("recovered", &graph, &constraints, 1.0)
1452 .expect("realize recovered loop");
1453 assert_eq!(realized.route.edges, route.edges);
1454 assert!(trail.support_points.len() >= 2);
1455 }
1456
1457 #[test]
1458 fn loop_design_admits_and_reverses_a_retraced_bridge() {
1459 let trailhead = Coord::new(0.0, 0.0);
1460 let junction = Coord::new(0.001, 0.0);
1461 let east = Coord::new(0.002, 0.0);
1462 let north = Coord::new(0.001, 0.001);
1463 let graph = GraphBuilder::default()
1464 .build(&[
1465 draft("bridge", trailhead, junction),
1466 draft("lower", junction, east),
1467 draft("upper", east, north),
1468 draft("west", north, junction),
1469 ])
1470 .expect("build lollipop network");
1471 let trail = Trail::forge(
1472 RouteShape::Loop,
1473 [trailhead, east, north]
1474 .map(|coord| SupportPoint::forge(coord).expect("valid support"))
1475 .to_vec(),
1476 RoutingLaw::default(),
1477 )
1478 .expect("valid loop design");
1479 let constraints = LoopConstraints {
1480 min_distance_m: 0.0,
1481 max_distance_m: f64::MAX,
1482 max_repeated_edge_fraction: 1.0,
1483 allowed_shapes: vec![
1484 RouteShape::Loop,
1485 RouteShape::FigureEight,
1486 RouteShape::OutAndBack,
1487 ],
1488 ..LoopConstraints::default()
1489 };
1490
1491 let realized = trail
1492 .realize("lollipop", &graph, &constraints, 1.0)
1493 .expect("a retraced bridge is a lawful manual loop");
1494
1495 assert_eq!(realized.trail.shape, RouteShape::Loop);
1496 assert_eq!(realized.route.metrics.shape, RouteShape::OutAndBack);
1497 assert!(realized.route.metrics.repeated_edge_fraction > 0.0);
1498 assert!(realized.route.verdict.satisfied);
1499 assert_eq!(
1500 graph.walk_edges(realized.route.start, &realized.route.edges),
1501 Some(realized.route.start)
1502 );
1503
1504 let reversal = realized
1505 .reverse_loop(&graph)
1506 .expect("closed design remains reversible despite its morphology");
1507 let reversed = reversal
1508 .trail
1509 .realize("lollipop", &graph, &constraints, 1.0)
1510 .expect("reversed supports reproduce the reversed closed walk");
1511 assert_eq!(reversed.trail.shape, RouteShape::Loop);
1512 assert_eq!(reversed.route.metrics.shape, RouteShape::OutAndBack);
1513 assert_ne!(realized.walk_fingerprint(), reversed.walk_fingerprint());
1514 let expected = realized
1515 .source_spans
1516 .iter()
1517 .rev()
1518 .map(|span| span.reversed())
1519 .collect::<Vec<_>>();
1520 assert!(same_spans(&reversed.source_spans, &expected));
1521 }
1522
1523 #[test]
1524 fn reversal_retains_pins_and_repairs_only_ambiguous_spans() {
1525 let trailhead = Coord::new(0.0, 0.0);
1526 let east = Coord::new(0.001, 0.0);
1527 let far_east = Coord::new(0.002, 0.0);
1528 let turn = Coord::new(0.002, 0.001);
1529 let detour = Coord::new(0.001, 0.001);
1530 let mut graph = GraphBuilder::default()
1531 .build(&[
1532 draft("head-east", trailhead, east),
1533 draft("east-far", east, far_east),
1534 draft("far-turn", far_east, turn),
1535 draft("turn-head", turn, trailhead),
1536 draft("turn-detour", turn, detour),
1537 draft("detour-head", detour, trailhead),
1538 ])
1539 .expect("build pentagonal network");
1540 let head_vertex = vertex_at(&graph, trailhead);
1541 let far_vertex = vertex_at(&graph, far_east);
1542 let turn_vertex = vertex_at(&graph, turn);
1543 graph.turn_bans.push(TurnBan {
1544 via: turn_vertex,
1545 from: edge_between(&graph, far_vertex, turn_vertex),
1546 to: edge_between(&graph, turn_vertex, head_vertex),
1547 provenance: Provenance::fixture("force-detour"),
1548 });
1549 graph.validate().expect("valid turn ban");
1550
1551 let supports = [trailhead, far_east, turn]
1552 .map(|coord| SupportPoint::forge(coord).expect("valid support"))
1553 .to_vec();
1554 let trail = Trail::forge(RouteShape::Loop, supports.clone(), RoutingLaw::default())
1555 .expect("valid loop design");
1556 let constraints = loop_constraints();
1557 let realized = trail
1558 .realize("detour", &graph, &constraints, 1.0)
1559 .expect("turn ban creates a lawful loop");
1560 let reversal = realized
1561 .reverse_loop(&graph)
1562 .expect("bidirectional loop is reversible");
1563
1564 assert_eq!(reversal.added_supports, 1);
1565 assert!(
1566 supports
1567 .iter()
1568 .all(|support| reversal.trail.support_points.contains(support))
1569 );
1570 let reversed = reversal
1571 .trail
1572 .realize("detour", &graph, &constraints, 1.0)
1573 .expect("repaired controls realize");
1574 let expected = realized
1575 .source_spans
1576 .iter()
1577 .rev()
1578 .map(|span| span.reversed())
1579 .collect::<Vec<_>>();
1580 assert!(same_spans(&reversed.source_spans, &expected));
1581 }
1582
1583 #[test]
1584 fn reversal_rejects_a_loop_containing_a_one_way_segment() {
1585 let a = Coord::new(0.0, 0.0);
1586 let b = Coord::new(0.001, 0.0);
1587 let c = Coord::new(0.0005, 0.001);
1588 let mut graph = GraphBuilder::default()
1589 .build(&[draft("ab", a, b), draft("bc", b, c), draft("ca", c, a)])
1590 .expect("build triangle");
1591 let va = vertex_at(&graph, a);
1592 let vb = vertex_at(&graph, b);
1593 let ab = edge_between(&graph, va, vb);
1594 graph.edges[ab.0].attr.travel = if graph.edges[ab.0].a == va {
1595 EdgeTravel::Forward
1596 } else {
1597 EdgeTravel::Backward
1598 };
1599 graph.rebuild_adjacency();
1600
1601 let supports = [a, b, c]
1602 .map(|coord| SupportPoint::forge(coord).expect("valid support"))
1603 .to_vec();
1604 let trail = Trail::forge(RouteShape::Loop, supports, RoutingLaw::default())
1605 .expect("valid loop design");
1606 let constraints = loop_constraints();
1607 let realized = trail
1608 .realize("one-way", &graph, &constraints, 1.0)
1609 .expect("forward direction is lawful");
1610
1611 assert!(matches!(
1612 realized.reverse_loop(&graph),
1613 Err(TrailgenError::OneWayReversal)
1614 ));
1615 }
1616
1617 #[test]
1618 fn support_insertion_follows_realized_leg_order() {
1619 let graph = graph();
1620 let constraints = LoopConstraints {
1621 min_distance_m: 0.0,
1622 max_distance_m: f64::MAX,
1623 max_repeated_edge_fraction: 0.0,
1624 allowed_shapes: vec![RouteShape::Loop],
1625 ..LoopConstraints::default()
1626 };
1627 let route = crate::ExactLoopSolver::default()
1628 .enumerate(&graph, VertexId(0), &constraints, 1)
1629 .into_iter()
1630 .next()
1631 .expect("fixture has a loop");
1632 let trail = Trail::infer(&graph, &route, RoutingLaw::default())
1633 .expect("loop has an exact support design");
1634 let realized = trail
1635 .realize("editable", &graph, &constraints, 1.0)
1636 .expect("realize recovered loop");
1637 let routed = realized.graph();
1638
1639 for slot in 1..realized.support_offsets.len() {
1640 let first = realized.support_offsets[slot - 1];
1641 let limit = realized.support_offsets[slot];
1642 if first == limit {
1643 continue;
1644 }
1645 let edge = &routed.edges[realized.route.edges[first].0];
1646 let insertion = realized
1647 .support_insertion(line_coord_at(
1648 &edge.geometry,
1649 edge.geometry.length_m() / 2.0,
1650 ))
1651 .expect("route edge admits insertion");
1652 assert_eq!(insertion.slot, slot);
1653 assert!(insertion.distance_m < 0.01);
1654 }
1655
1656 let closure = *realized.support_offsets.last().expect("support offset");
1657 if closure < realized.route.edges.len() {
1658 let edge = &routed.edges[realized.route.edges[closure].0];
1659 let insertion = realized
1660 .support_insertion(line_coord_at(
1661 &edge.geometry,
1662 edge.geometry.length_m() / 2.0,
1663 ))
1664 .expect("closure edge admits insertion");
1665 assert_eq!(insertion.slot, realized.bindings.len());
1666 }
1667 }
1668
1669 #[test]
1670 fn non_shortest_candidate_edges_recover_lossless_interior_supports() {
1671 let a = Coord::new(0.0, 0.0);
1672 let b = Coord::new(0.002, 0.0);
1673 let c = Coord::new(0.001, 0.000_4);
1674 let draft = |name: &str, from: Coord, to: Coord, road_exposure| SegmentDraft {
1675 geometry: LineString::new(vec![from, to]).expect("valid segment"),
1676 junctions: JunctionPolicy::Planar,
1677 turn_ref: None,
1678 junction_keys: None,
1679 turn_restrictions: Vec::new(),
1680 way_kind: WayKind::Path,
1681 realm: WayRealm::default(),
1682 geometry_claim: GeometryClaim::default(),
1683 crossing_control: CrossingControl::default(),
1684 standing: TrailStanding::Established,
1685 marking: crate::TrailMarking::default(),
1686 terrain: Terrain::Trail,
1687 terrain_confidence: Some(1.0),
1688 surface: Some("dirt".to_owned()),
1689 access: Access::Open,
1690 travel: EdgeTravel::Both,
1691 road_exposure,
1692 confidence: 1.0,
1693 provenance: vec![Provenance::fixture(name)],
1694 };
1695 let graph = GraphBuilder::default()
1696 .build(&[
1697 draft("direct", a, b, 1.0),
1698 draft("detour-a", a, c, 0.0),
1699 draft("detour-b", c, b, 0.0),
1700 ])
1701 .expect("build triangle");
1702 let vertex = |coord: Coord| {
1703 graph
1704 .vertices
1705 .iter()
1706 .find(|vertex| vertex.coord.planar_distance2(coord) < 1.0e-16)
1707 .expect("coordinate vertex")
1708 .id
1709 };
1710 let edge = |from: VertexId, to: VertexId| {
1711 graph.adjacency[from.0]
1712 .iter()
1713 .copied()
1714 .find(|edge| graph.edges[edge.0].other(from) == Some(to))
1715 .expect("adjacent vertices")
1716 };
1717 let va = vertex(a);
1718 let vb = vertex(b);
1719 let vc = vertex(c);
1720 let original = vec![edge(va, vb), edge(vb, vc), edge(vc, va)];
1721 let constraints = LoopConstraints {
1722 min_distance_m: 0.0,
1723 max_distance_m: f64::MAX,
1724 max_repeated_edge_fraction: 0.0,
1725 allowed_shapes: vec![RouteShape::Loop],
1726 ..LoopConstraints::default()
1727 };
1728 let route = Route::from_edges("non-shortest", &graph, va, original, &constraints);
1729 let trail = Trail::infer(&graph, &route, RoutingLaw::default())
1730 .expect("non-shortest edge gains an interior support");
1731 assert!(
1732 trail.support_points.len() >= 3,
1733 "interior support should make the direct road edge compulsory"
1734 );
1735 let realized = trail
1736 .realize("recovered", &graph, &constraints, 1.0)
1737 .expect("realize inferred controls");
1738 assert_eq!(realized.route.metrics.shape, RouteShape::Loop);
1739 assert!((realized.route.metrics.distance_m - route.metrics.distance_m).abs() < 0.01);
1740 }
1741}