Skip to main content

trailgen_core/
trail.rs

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    /// Recovers a compact, lossless support design. Globally shortest spans
130    /// remain single legs; an irreducible non-shortest edge receives an
131    /// interior support so the design compels that physical segment.
132    #[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                    // Prefer a simple closure, then admit the shortest lawful
305                    // closed walk when the network topology compels a retrace.
306                    .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
335/// A trail shape is design intent; route shape is observed walk morphology.
336/// In particular, a manually designed loop may revisit a junction or retrace
337/// an edge while still fulfilling its sole topological promise: returning to
338/// its trailhead.
339fn 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    /// Direction-sensitive identity for this realization's ordered physical
487    /// walk. The value is an ephemeral comparison key, not durable project
488    /// identity or a cryptographic digest.
489    #[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    /// Inverts a loop's exact physical walk while retaining every existing
510    /// support. The support tail is reversed; compact repairs are inserted
511    /// only where that program would otherwise select another path.
512    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    /// Locates a new support in the design order of the realized walk. Loops
582    /// admit their closing arc after the final explicit support; out-and-backs
583    /// index only their outward spine because the return is its exact reverse.
584    #[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}