Skip to main content

trailgen_core/
builder.rs

1use crate::enrich::{EmbeddedElevation, EnrichmentConfig, enrich_graph};
2use crate::geo::{Coord, LineString};
3use crate::model::{
4    Access, CrossingControl, Edge, EdgeAttr, EdgeId, EdgeTravel, GeometryClaim, GradeDistribution,
5    Provenance, Terrain, TrailMarking, TrailStanding, TurnBan, Vertex, VertexId, WalkGraph,
6    WayKind, WayRealm,
7};
8use crate::{Result, TrailgenError};
9use rstar::{AABB, RTree, RTreeObject};
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, btree_map::Entry};
12
13pub const DEFAULT_SNAP_TOLERANCE_M: f64 = 15.0;
14
15#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
16#[serde(transparent)]
17pub struct JunctionKey(pub String);
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
20pub struct SegmentDraft {
21    pub geometry: LineString,
22    #[serde(default)]
23    pub junctions: JunctionPolicy,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub turn_ref: Option<String>,
26    /// Provider-owned endpoint identity. When present, coordinates are shape;
27    /// only equal keys join topology.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub junction_keys: Option<[JunctionKey; 2]>,
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub turn_restrictions: Vec<TurnRestrictionDraft>,
32    #[serde(default)]
33    pub way_kind: WayKind,
34    #[serde(default)]
35    pub realm: WayRealm,
36    #[serde(default)]
37    pub geometry_claim: GeometryClaim,
38    #[serde(default)]
39    pub crossing_control: CrossingControl,
40    #[serde(default)]
41    pub standing: TrailStanding,
42    #[serde(default)]
43    pub marking: TrailMarking,
44    pub terrain: Terrain,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub terrain_confidence: Option<f64>,
47    pub surface: Option<String>,
48    pub access: Access,
49    #[serde(default)]
50    pub travel: EdgeTravel,
51    pub road_exposure: f64,
52    pub confidence: f64,
53    pub provenance: Vec<Provenance>,
54}
55
56impl SegmentDraft {
57    /// Cut source geometry without counterfeiting provider-owned junctions.
58    /// Artificial endpoints are namespaced to this source span, so coincident
59    /// clips of distinct facilities remain distinct topology.
60    #[must_use]
61    pub fn fragment(&self, geometry: LineString) -> Self {
62        let whole = same_location(geometry.start(), self.geometry.start())
63            && same_location(geometry.end(), self.geometry.end());
64        let mut fragment = self.clone();
65        fragment.turn_restrictions.retain(|restriction| {
66            geometry
67                .points
68                .iter()
69                .any(|point| same_location(*point, restriction.via))
70        });
71        fragment.junction_keys = self.junction_keys.as_ref().map(|[a, b]| {
72            let namespace = format!("{}→{}", a.0, b.0);
73            [
74                fragment_junction(a, self.geometry.start(), geometry.start(), &namespace),
75                fragment_junction(b, self.geometry.end(), geometry.end(), &namespace),
76            ]
77        });
78        fragment.geometry = geometry;
79        if !whole && fragment.junctions == JunctionPolicy::ExplicitNodes {
80            fragment.junctions = JunctionPolicy::ExplicitEndpoints;
81        }
82        fragment
83    }
84}
85
86fn fragment_junction(
87    source: &JunctionKey,
88    source_coord: Coord,
89    fragment_coord: Coord,
90    namespace: &str,
91) -> JunctionKey {
92    if same_location(source_coord, fragment_coord) {
93        source.clone()
94    } else {
95        JunctionKey(format!(
96            "clip:{namespace}:{:016x}:{:016x}",
97            fragment_coord.lon.to_bits(),
98            fragment_coord.lat.to_bits()
99        ))
100    }
101}
102
103const fn same_location(left: Coord, right: Coord) -> bool {
104    left.lon.to_bits() == right.lon.to_bits() && left.lat.to_bits() == right.lat.to_bits()
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
108#[serde(rename_all = "kebab-case")]
109pub enum JunctionPolicy {
110    #[default]
111    Planar,
112    ExplicitNodes,
113    ExplicitEndpoints,
114    GradeSeparatedEndpoints,
115}
116
117#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
118pub struct TurnRestrictionDraft {
119    pub from: String,
120    pub via: Coord,
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub via_key: Option<JunctionKey>,
123    pub to: String,
124    pub rule: TurnRestrictionRule,
125    pub provenance: Provenance,
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "kebab-case")]
130pub enum TurnRestrictionRule {
131    No,
132    Only,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
136pub struct GraphBuilder {
137    pub snap_tolerance_m: f64,
138    pub enrichment: EnrichmentConfig,
139}
140
141impl Default for GraphBuilder {
142    fn default() -> Self {
143        Self {
144            snap_tolerance_m: DEFAULT_SNAP_TOLERANCE_M,
145            enrichment: EnrichmentConfig::default(),
146        }
147    }
148}
149
150#[derive(Clone, Copy)]
151struct Primitive {
152    a: Coord,
153    b: Coord,
154    src: usize,
155}
156
157#[derive(Clone, Copy)]
158struct SnapPrimitive {
159    a: Coord,
160    b: Coord,
161    primitive: usize,
162    start_t: f64,
163    end_t: f64,
164}
165
166#[derive(Clone, Copy)]
167struct PrimitiveEnvelope {
168    index: usize,
169    envelope: AABB<[f64; 2]>,
170}
171
172impl RTreeObject for PrimitiveEnvelope {
173    type Envelope = AABB<[f64; 2]>;
174
175    fn envelope(&self) -> Self::Envelope {
176        self.envelope
177    }
178}
179
180#[derive(Clone, Copy)]
181struct Cut {
182    t: f64,
183    coord: Coord,
184    snapped: bool,
185}
186
187impl Cut {
188    const fn exact(t: f64, coord: Coord) -> Self {
189        Self {
190            t,
191            coord,
192            snapped: false,
193        }
194    }
195
196    const fn snapped(t: f64, coord: Coord) -> Self {
197        Self {
198            t,
199            coord,
200            snapped: true,
201        }
202    }
203}
204
205#[derive(Clone, Copy)]
206struct SnapCandidate {
207    src_primitive: usize,
208    src_t: f64,
209    target_primitive: usize,
210    target_t: f64,
211    coord: Coord,
212    distance2: f64,
213    src_endpoint: usize,
214    target_endpoint: Option<usize>,
215}
216
217impl GraphBuilder {
218    pub fn build(self, drafts: &[SegmentDraft]) -> Result<WalkGraph> {
219        if drafts.is_empty() {
220            return Err(TrailgenError::InvalidData(
221                "cannot build graph from zero segments".to_owned(),
222            ));
223        }
224
225        let primitives = draft_primitives(drafts);
226        let snap_primitives = snap_primitives(drafts, &primitives);
227
228        let mut cuts = primitives
229            .iter()
230            .map(|p| vec![Cut::exact(0.0, p.a), Cut::exact(1.0, p.b)])
231            .collect::<Vec<_>>();
232
233        let index = primitive_index(&primitives);
234        let snap_index = snap_primitive_index(&snap_primitives);
235        for (i, primitive) in primitives.iter().copied().enumerate() {
236            for candidate in index.locate_in_envelope_intersecting(primitive_envelope(primitive)) {
237                let j = candidate.index;
238                if j <= i {
239                    continue;
240                }
241                if !junctions_may_be_inferred(drafts, primitives[i], primitives[j]) {
242                    continue;
243                }
244                if let Some((t, u, c)) = segment_intersection(primitives[i], primitives[j])
245                    && (0.0..=1.0).contains(&t)
246                    && (0.0..=1.0).contains(&u)
247                {
248                    cuts[i].push(Cut::exact(t, c));
249                    cuts[j].push(Cut::exact(u, c));
250                }
251            }
252        }
253
254        for snap in near_miss_snaps(
255            drafts,
256            &primitives,
257            &snap_primitives,
258            &snap_index,
259            self.snap_tolerance_m,
260        ) {
261            cuts[snap.src_primitive].push(Cut::snapped(snap.src_t, snap.coord));
262            cuts[snap.target_primitive].push(Cut::snapped(snap.target_t, snap.coord));
263        }
264
265        let assembly = assemble_edges(drafts, &primitives, cuts, self.snap_tolerance_m);
266        let mut graph = WalkGraph::new(assembly.vertices, assembly.edges);
267        graph.turn_bans = turn_bans(
268            drafts,
269            &assembly.edges_by_draft,
270            &graph,
271            self.snap_tolerance_m,
272        );
273        enrich_graph(&mut graph, &EmbeddedElevation, self.enrichment)?;
274        Ok(graph)
275    }
276}
277
278struct EdgeAssembly {
279    vertices: Vec<Vertex>,
280    edges: Vec<Edge>,
281    edges_by_draft: Vec<Vec<EdgeId>>,
282}
283
284#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
285enum VertexKey {
286    Coordinate(u64, u64),
287    Source(JunctionKey),
288}
289
290fn assemble_edges(
291    drafts: &[SegmentDraft],
292    primitives: &[Primitive],
293    cuts: Vec<Vec<Cut>>,
294    snap_tolerance_m: f64,
295) -> EdgeAssembly {
296    let mut vertices = Vec::<Vertex>::new();
297    let mut vertex_by_key = BTreeMap::<VertexKey, VertexId>::new();
298    let mut edges = Vec::<Edge>::new();
299    let mut edge_by_support =
300        BTreeMap::<(VertexId, VertexId, Vec<(u64, u64)>, WayKind, GeometryClaim), EdgeId>::new();
301    let mut edges_by_draft = vec![Vec::<EdgeId>::new(); drafts.len()];
302    let snap_provenance = Provenance {
303        source: "graph-builder".to_owned(),
304        layer: Some("near-miss-snap".to_owned()),
305        source_id: Some(format!("tolerance {snap_tolerance_m:.1} m")),
306        license: None,
307    };
308
309    for (primitive, xs) in primitives.iter().copied().zip(cuts) {
310        let xs = normalize_cuts(xs);
311        for pair in xs.windows(2) {
312            let a = pair[0].coord;
313            let b = pair[1].coord;
314            if a.planar_distance2(b) < 1.0e-18 {
315                continue;
316            }
317            let draft = &drafts[primitive.src];
318            let va = vertex_id(
319                a,
320                endpoint_key(draft, pair[0].t),
321                &mut vertices,
322                &mut vertex_by_key,
323            );
324            let vb = vertex_id(
325                b,
326                endpoint_key(draft, pair[1].t),
327                &mut vertices,
328                &mut vertex_by_key,
329            );
330            if va == vb {
331                continue;
332            }
333            let geometry = edge_geometry(
334                draft,
335                vertices[va.0].coord,
336                vertices[vb.0].coord,
337                pair[0].t,
338                pair[1].t,
339            );
340            let snapped = pair[0].snapped || pair[1].snapped;
341            let attr = edge_attr(draft, &geometry, snapped.then_some(snap_provenance.clone()));
342            let id = EdgeId(edges.len());
343            let mut edge = Edge {
344                id,
345                a: va,
346                b: vb,
347                geometry,
348                attr,
349            };
350            crate::hiking::HikingModel.apply(&mut edge);
351            let support = support_key(&edge);
352            let id = if let Some(id) = edge_by_support.get(&support).copied() {
353                corroborate_edge(&mut edges[id.0], &edge);
354                crate::hiking::HikingModel.apply(&mut edges[id.0]);
355                id
356            } else {
357                edge_by_support.insert(support, id);
358                edges.push(edge);
359                id
360            };
361            edges_by_draft[primitive.src].push(id);
362        }
363    }
364
365    EdgeAssembly {
366        vertices,
367        edges,
368        edges_by_draft,
369    }
370}
371
372fn support_key(edge: &Edge) -> (VertexId, VertexId, Vec<(u64, u64)>, WayKind, GeometryClaim) {
373    let forward = edge
374        .geometry
375        .points
376        .iter()
377        .map(|point| (point.lon.to_bits(), point.lat.to_bits()));
378    let reverse = edge
379        .geometry
380        .points
381        .iter()
382        .rev()
383        .map(|point| (point.lon.to_bits(), point.lat.to_bits()));
384    let forward = forward.collect::<Vec<_>>();
385    let reverse = reverse.collect::<Vec<_>>();
386    if (edge.a, edge.b, &forward) <= (edge.b, edge.a, &reverse) {
387        (
388            edge.a,
389            edge.b,
390            forward,
391            edge.attr.way_kind,
392            edge.attr.geometry_claim,
393        )
394    } else {
395        (
396            edge.b,
397            edge.a,
398            reverse,
399            edge.attr.way_kind,
400            edge.attr.geometry_claim,
401        )
402    }
403}
404
405fn endpoint_key(draft: &SegmentDraft, progress: f64) -> Option<JunctionKey> {
406    let keys = draft.junction_keys.as_ref()?;
407    if progress <= 1.0e-12 {
408        Some(keys[0].clone())
409    } else if progress >= 1.0 - 1.0e-12 {
410        Some(keys[1].clone())
411    } else {
412        None
413    }
414}
415
416fn corroborate_edge(preferred: &mut Edge, suppressed: &Edge) {
417    for provenance in &suppressed.attr.provenance {
418        if !preferred.attr.provenance.contains(provenance) {
419            preferred.attr.provenance.push(provenance.clone());
420        }
421    }
422    preferred.attr.confidence = preferred.attr.confidence.max(suppressed.attr.confidence);
423    if preferred.attr.way_kind == WayKind::Unknown {
424        preferred.attr.way_kind = suppressed.attr.way_kind;
425    }
426    if preferred.attr.standing == TrailStanding::Unknown {
427        preferred.attr.standing = suppressed.attr.standing;
428    }
429    if preferred.attr.marking == TrailMarking::Unknown {
430        preferred.attr.marking = suppressed.attr.marking;
431    }
432    if preferred.attr.terrain == Terrain::Unknown {
433        preferred.attr.terrain = suppressed.attr.terrain;
434        preferred.attr.terrain_confidence = suppressed.attr.terrain_confidence;
435    }
436    if preferred.attr.surface.is_none() {
437        preferred.attr.surface.clone_from(&suppressed.attr.surface);
438    }
439    if preferred.attr.access == Access::Unknown {
440        preferred.attr.access = suppressed.attr.access;
441    }
442}
443
444fn draft_primitives(drafts: &[SegmentDraft]) -> Vec<Primitive> {
445    drafts
446        .iter()
447        .enumerate()
448        .flat_map(|(src, draft)| {
449            if matches!(
450                draft.junctions,
451                JunctionPolicy::ExplicitEndpoints | JunctionPolicy::GradeSeparatedEndpoints
452            ) {
453                let points = &draft.geometry.points;
454                vec![Primitive {
455                    a: points[0],
456                    b: points[points.len() - 1],
457                    src,
458                }]
459            } else {
460                draft
461                    .geometry
462                    .points
463                    .windows(2)
464                    .map(|points| Primitive {
465                        a: points[0],
466                        b: points[1],
467                        src,
468                    })
469                    .collect()
470            }
471        })
472        .collect()
473}
474
475fn snap_primitives(drafts: &[SegmentDraft], primitives: &[Primitive]) -> Vec<SnapPrimitive> {
476    primitives
477        .iter()
478        .copied()
479        .enumerate()
480        .flat_map(|(primitive, topology)| {
481            let draft = &drafts[topology.src];
482            if !matches!(
483                draft.junctions,
484                JunctionPolicy::ExplicitEndpoints | JunctionPolicy::GradeSeparatedEndpoints
485            ) {
486                return vec![SnapPrimitive {
487                    a: topology.a,
488                    b: topology.b,
489                    primitive,
490                    start_t: 0.0,
491                    end_t: 1.0,
492                }];
493            }
494            let lengths = draft
495                .geometry
496                .points
497                .windows(2)
498                .map(|segment| segment[0].haversine_m(segment[1]))
499                .collect::<Vec<_>>();
500            let total_m = lengths.iter().sum::<f64>().max(f64::EPSILON);
501            let mut traversed_m = 0.0;
502            draft
503                .geometry
504                .points
505                .windows(2)
506                .zip(lengths)
507                .map(|(segment, length_m)| {
508                    let start_t = traversed_m / total_m;
509                    traversed_m += length_m;
510                    SnapPrimitive {
511                        a: segment[0],
512                        b: segment[1],
513                        primitive,
514                        start_t,
515                        end_t: traversed_m / total_m,
516                    }
517                })
518                .collect()
519        })
520        .collect()
521}
522
523fn edge_geometry(draft: &SegmentDraft, a: Coord, b: Coord, start_t: f64, end_t: f64) -> LineString {
524    if !matches!(
525        draft.junctions,
526        JunctionPolicy::ExplicitEndpoints | JunctionPolicy::GradeSeparatedEndpoints
527    ) {
528        return LineString::unchecked(vec![a, b]);
529    }
530    let lengths = draft
531        .geometry
532        .points
533        .windows(2)
534        .map(|segment| segment[0].haversine_m(segment[1]))
535        .collect::<Vec<_>>();
536    let total_m = lengths.iter().sum::<f64>();
537    let start_m = total_m * start_t;
538    let end_m = total_m * end_t;
539    let mut traversed_m = 0.0;
540    let mut points = vec![a];
541    for (index, length_m) in lengths.into_iter().enumerate() {
542        traversed_m += length_m;
543        if traversed_m > start_m + 1.0e-7 && traversed_m < end_m - 1.0e-7 {
544            points.push(draft.geometry.points[index + 1]);
545        }
546    }
547    points.push(b);
548    LineString::unchecked(points)
549}
550
551fn junctions_may_be_inferred(drafts: &[SegmentDraft], a: Primitive, b: Primitive) -> bool {
552    drafts[a.src].junctions == JunctionPolicy::Planar
553        && drafts[b.src].junctions == JunctionPolicy::Planar
554}
555
556fn snaps_may_be_inferred(drafts: &[SegmentDraft], a: Primitive, b: Primitive) -> bool {
557    if drafts[a.src].junction_keys.is_some() || drafts[b.src].junction_keys.is_some() {
558        return false;
559    }
560    let inferable = |policy| {
561        matches!(
562            policy,
563            JunctionPolicy::Planar | JunctionPolicy::ExplicitEndpoints
564        )
565    };
566    inferable(drafts[a.src].junctions) && inferable(drafts[b.src].junctions)
567}
568
569fn primitive_index(primitives: &[Primitive]) -> RTree<PrimitiveEnvelope> {
570    RTree::bulk_load(
571        primitives
572            .iter()
573            .copied()
574            .enumerate()
575            .map(|(index, primitive)| PrimitiveEnvelope {
576                index,
577                envelope: primitive_envelope(primitive),
578            })
579            .collect(),
580    )
581}
582
583fn snap_primitive_index(primitives: &[SnapPrimitive]) -> RTree<PrimitiveEnvelope> {
584    RTree::bulk_load(
585        primitives
586            .iter()
587            .enumerate()
588            .map(|(index, primitive)| PrimitiveEnvelope {
589                index,
590                envelope: AABB::from_corners(
591                    [
592                        primitive.a.lon.min(primitive.b.lon),
593                        primitive.a.lat.min(primitive.b.lat),
594                    ],
595                    [
596                        primitive.a.lon.max(primitive.b.lon),
597                        primitive.a.lat.max(primitive.b.lat),
598                    ],
599                ),
600            })
601            .collect(),
602    )
603}
604
605fn primitive_envelope(p: Primitive) -> AABB<[f64; 2]> {
606    AABB::from_corners(
607        [p.a.lon.min(p.b.lon), p.a.lat.min(p.b.lat)],
608        [p.a.lon.max(p.b.lon), p.a.lat.max(p.b.lat)],
609    )
610}
611
612fn turn_bans(
613    drafts: &[SegmentDraft],
614    edges_by_draft: &[Vec<EdgeId>],
615    graph: &WalkGraph,
616    snap_tolerance_m: f64,
617) -> Vec<TurnBan> {
618    let mut edges_by_ref = BTreeMap::<&str, Vec<EdgeId>>::new();
619    for (draft, edges) in drafts.iter().zip(edges_by_draft) {
620        if let Some(turn_ref) = draft.turn_ref.as_deref() {
621            edges_by_ref.entry(turn_ref).or_default().extend(edges);
622        }
623    }
624    let restrictions = drafts
625        .iter()
626        .flat_map(|draft| draft.turn_restrictions.iter())
627        .collect::<Vec<_>>();
628    let mut seen = std::collections::BTreeSet::<(VertexId, EdgeId, EdgeId)>::new();
629    let mut bans = Vec::new();
630    for restriction in restrictions {
631        let via = if let Some(key) = &restriction.via_key {
632            let Some(vertex) = graph
633                .vertices
634                .iter()
635                .find(|vertex| vertex.junction.as_ref() == Some(key))
636                .map(|vertex| vertex.id)
637            else {
638                continue;
639            };
640            vertex
641        } else {
642            let Some((vertex, distance_m)) = graph.nearest_vertex_with_distance(restriction.via)
643            else {
644                continue;
645            };
646            if distance_m > snap_tolerance_m.max(1.0) {
647                continue;
648            }
649            vertex
650        };
651        let Some(from_edges) = edges_by_ref.get(restriction.from.as_str()) else {
652            continue;
653        };
654        let Some(to_edges) = edges_by_ref.get(restriction.to.as_str()) else {
655            continue;
656        };
657        let from_edges = from_edges
658            .iter()
659            .copied()
660            .filter(|edge| arrives_at(graph, *edge, via))
661            .collect::<Vec<_>>();
662        let allowed = to_edges
663            .iter()
664            .copied()
665            .filter(|edge| departs_from(graph, *edge, via))
666            .collect::<std::collections::BTreeSet<_>>();
667        let banned = match restriction.rule {
668            TurnRestrictionRule::No => allowed.iter().copied().collect::<Vec<_>>(),
669            TurnRestrictionRule::Only => graph.adjacency[via.0]
670                .iter()
671                .copied()
672                .filter(|edge| !allowed.contains(edge))
673                .collect(),
674        };
675        for from in &from_edges {
676            for to in &banned {
677                if seen.insert((via, *from, *to)) {
678                    bans.push(TurnBan {
679                        via,
680                        from: *from,
681                        to: *to,
682                        provenance: restriction.provenance.clone(),
683                    });
684                }
685            }
686        }
687    }
688    bans
689}
690
691fn arrives_at(graph: &WalkGraph, edge: EdgeId, via: VertexId) -> bool {
692    let edge = &graph.edges[edge.0];
693    edge.other(via)
694        .is_some_and(|other| edge.traverse(other) == Some(via))
695}
696
697fn departs_from(graph: &WalkGraph, edge: EdgeId, via: VertexId) -> bool {
698    graph.edges[edge.0].traverse(via).is_some()
699}
700
701fn edge_attr(
702    draft: &SegmentDraft,
703    geometry: &LineString,
704    snap_provenance: Option<Provenance>,
705) -> EdgeAttr {
706    let (ascent_m, descent_m) = geometry.ascent_descent_m();
707    let length_m = geometry.length_m();
708    let grade_abs_mean = if length_m > 0.0 {
709        (ascent_m + descent_m) / length_m
710    } else {
711        0.0
712    };
713    let mut provenance = draft.provenance.clone();
714    let mut confidence = draft.confidence.clamp(0.0, 1.0);
715    if let Some(snap_provenance) = snap_provenance {
716        provenance.push(snap_provenance);
717        confidence = confidence.min(0.74);
718    }
719    EdgeAttr {
720        length_m,
721        ascent_m,
722        descent_m,
723        grade_abs_mean,
724        grade_abs_max: grade_abs_mean,
725        sustained_steep_m: 0.0,
726        grade_distribution: GradeDistribution::default().add_segment(length_m, grade_abs_mean),
727        hill_slope_deg: None,
728        way_kind: draft.way_kind,
729        realm: draft.realm,
730        geometry_claim: draft.geometry_claim,
731        crossing_control: draft.crossing_control,
732        standing: draft.standing,
733        marking: draft.marking,
734        terrain: draft.terrain,
735        surface: draft.surface.clone(),
736        terrain_confidence: draft
737            .terrain_confidence
738            .unwrap_or_else(|| legacy_terrain_confidence(draft.terrain))
739            .clamp(0.0, 1.0),
740        terrain_evidence: Vec::new(),
741        access: draft.access,
742        travel: draft.travel,
743        access_confidence: if draft.access == Access::Unknown {
744            0.0
745        } else {
746            0.90
747        },
748        access_provenance: Vec::new(),
749        crossings: Vec::new(),
750        road_exposure: draft.road_exposure.clamp(0.0, 1.0),
751        confidence,
752        traversal: crate::hiking::EdgeTraversal::default(),
753        seed_count: 0,
754        popularity: 0.0,
755        seed_provenance: Vec::new(),
756        elevation_provenance: Vec::new(),
757        provenance,
758    }
759}
760
761const fn legacy_terrain_confidence(terrain: Terrain) -> f64 {
762    if matches!(terrain, Terrain::Unknown) {
763        0.0
764    } else {
765        0.90
766    }
767}
768
769fn normalize_cuts(mut cuts: Vec<Cut>) -> Vec<Cut> {
770    cuts.sort_by(|a, b| a.t.total_cmp(&b.t));
771    let mut normalized = Vec::<Cut>::new();
772    for cut in cuts {
773        let Some(last) = normalized.last_mut() else {
774            normalized.push(cut);
775            continue;
776        };
777        if (last.t - cut.t).abs() < 1.0e-9 {
778            if cut.snapped {
779                *last = cut;
780            }
781        } else {
782            normalized.push(cut);
783        }
784    }
785    normalized
786}
787
788fn near_miss_snaps(
789    drafts: &[SegmentDraft],
790    primitives: &[Primitive],
791    snap_primitives: &[SnapPrimitive],
792    index: &RTree<PrimitiveEnvelope>,
793    tolerance_m: f64,
794) -> Vec<SnapCandidate> {
795    let candidates = near_miss_candidates(drafts, primitives, snap_primitives, index, tolerance_m);
796    let (mut snaps, clustered) = cluster_endpoint_snaps(&candidates, primitives, tolerance_m);
797    let mut best_interior = BTreeMap::<usize, SnapCandidate>::new();
798    for candidate in candidates.into_iter().filter(|candidate| {
799        candidate.target_endpoint.is_none() && !clustered[candidate.src_endpoint]
800    }) {
801        match best_interior.entry(candidate.src_endpoint) {
802            Entry::Occupied(mut entry) if snap_rank(candidate) < snap_rank(*entry.get()) => {
803                entry.insert(candidate);
804            }
805            Entry::Vacant(entry) => {
806                entry.insert(candidate);
807            }
808            Entry::Occupied(_) => {}
809        }
810    }
811    snaps.extend(best_interior.into_values());
812    snaps
813}
814
815fn near_miss_candidates(
816    drafts: &[SegmentDraft],
817    primitives: &[Primitive],
818    snap_primitives: &[SnapPrimitive],
819    index: &RTree<PrimitiveEnvelope>,
820    tolerance_m: f64,
821) -> Vec<SnapCandidate> {
822    let mut candidates = Vec::new();
823    for (src_idx, primitive) in primitives.iter().copied().enumerate() {
824        for (endpoint_ix, endpoint_t, endpoint) in [(0, 0.0, primitive.a), (1, 1.0, primitive.b)] {
825            let src_endpoint = src_idx * 2 + endpoint_ix;
826            let latitude_radius = tolerance_m / 110_540.0;
827            let longitude_radius =
828                tolerance_m / (111_320.0 * endpoint.lat.to_radians().cos().abs().max(0.05));
829            let neighborhood = AABB::from_corners(
830                [
831                    endpoint.lon - longitude_radius,
832                    endpoint.lat - latitude_radius,
833                ],
834                [
835                    endpoint.lon + longitude_radius,
836                    endpoint.lat + latitude_radius,
837                ],
838            );
839            for candidate in index.locate_in_envelope_intersecting(neighborhood) {
840                let target_segment = snap_primitives[candidate.index];
841                let target_idx = target_segment.primitive;
842                let target = primitives[target_idx];
843                if src_idx == target_idx || primitive.src == target.src {
844                    continue;
845                }
846                if !snaps_may_be_inferred(drafts, primitive, target) {
847                    continue;
848                }
849                let Some((segment_t, coord, distance2)) = projected_snap(
850                    endpoint,
851                    target_segment.a,
852                    target_segment.b,
853                    tolerance_m * tolerance_m,
854                ) else {
855                    continue;
856                };
857                let target_t = (target_segment.end_t - target_segment.start_t)
858                    .mul_add(segment_t, target_segment.start_t);
859                let target_endpoint = if target_t <= 1.0e-9 {
860                    Some(target_idx * 2)
861                } else if target_t >= 1.0 - 1.0e-9 {
862                    Some(target_idx * 2 + 1)
863                } else {
864                    None
865                };
866                if target_endpoint.is_some_and(|target| src_endpoint > target) {
867                    continue;
868                }
869                candidates.push(SnapCandidate {
870                    src_primitive: src_idx,
871                    src_t: endpoint_t,
872                    target_primitive: target_idx,
873                    target_t,
874                    coord,
875                    distance2,
876                    src_endpoint,
877                    target_endpoint,
878                });
879            }
880        }
881    }
882    candidates
883}
884
885fn cluster_endpoint_snaps(
886    candidates: &[SnapCandidate],
887    primitives: &[Primitive],
888    tolerance_m: f64,
889) -> (Vec<SnapCandidate>, Vec<bool>) {
890    let endpoint_count = primitives.len() * 2;
891    let mut parent = (0..endpoint_count).collect::<Vec<_>>();
892    let mut members = (0..endpoint_count)
893        .map(|endpoint| vec![endpoint])
894        .collect::<Vec<_>>();
895    let mut endpoint_candidates = candidates
896        .iter()
897        .filter(|candidate| candidate.target_endpoint.is_some())
898        .copied()
899        .collect::<Vec<_>>();
900    endpoint_candidates.sort_by_key(|candidate| {
901        (
902            candidate.distance2.to_bits(),
903            candidate.src_endpoint,
904            candidate.target_endpoint,
905        )
906    });
907    for candidate in endpoint_candidates {
908        let target = candidate.target_endpoint.expect("filtered endpoint snap");
909        let a = endpoint_root(&parent, candidate.src_endpoint);
910        let b = endpoint_root(&parent, target);
911        if a == b || !clusters_fit(&members[a], &members[b], primitives, tolerance_m) {
912            continue;
913        }
914        let (keep, discard) = if a < b { (a, b) } else { (b, a) };
915        parent[discard] = keep;
916        let displaced = std::mem::take(&mut members[discard]);
917        members[keep].extend(displaced);
918    }
919
920    let mut snaps = Vec::new();
921    let mut clustered = vec![false; endpoint_count];
922    for cluster in members.iter().filter(|cluster| cluster.len() > 1) {
923        let canonical = endpoint_medoid(cluster, primitives);
924        let coord = endpoint_coord(primitives, canonical);
925        for &endpoint in cluster {
926            clustered[endpoint] = true;
927            if endpoint == canonical {
928                continue;
929            }
930            snaps.push(SnapCandidate {
931                src_primitive: endpoint / 2,
932                src_t: endpoint_t(endpoint),
933                target_primitive: canonical / 2,
934                target_t: endpoint_t(canonical),
935                coord,
936                distance2: endpoint_coord(primitives, endpoint)
937                    .haversine_m(coord)
938                    .powi(2),
939                src_endpoint: endpoint,
940                target_endpoint: Some(canonical),
941            });
942        }
943    }
944    (snaps, clustered)
945}
946
947const fn snap_rank(candidate: SnapCandidate) -> (u64, usize, u64) {
948    (
949        candidate.distance2.to_bits(),
950        candidate.target_primitive,
951        candidate.target_t.to_bits(),
952    )
953}
954
955const fn endpoint_t(endpoint: usize) -> f64 {
956    if endpoint.is_multiple_of(2) { 0.0 } else { 1.0 }
957}
958
959fn endpoint_root(parent: &[usize], mut endpoint: usize) -> usize {
960    while parent[endpoint] != endpoint {
961        endpoint = parent[endpoint];
962    }
963    endpoint
964}
965
966fn endpoint_coord(primitives: &[Primitive], endpoint: usize) -> Coord {
967    let primitive = primitives[endpoint / 2];
968    if endpoint.is_multiple_of(2) {
969        primitive.a
970    } else {
971        primitive.b
972    }
973}
974
975fn clusters_fit(a: &[usize], b: &[usize], primitives: &[Primitive], tolerance_m: f64) -> bool {
976    a.iter().all(|&lhs| {
977        b.iter().all(|&rhs| {
978            endpoint_coord(primitives, lhs).haversine_m(endpoint_coord(primitives, rhs))
979                <= tolerance_m
980        })
981    })
982}
983
984fn endpoint_medoid(cluster: &[usize], primitives: &[Primitive]) -> usize {
985    cluster
986        .iter()
987        .copied()
988        .min_by(|&lhs, &rhs| {
989            let score = |candidate| {
990                cluster
991                    .iter()
992                    .map(|&other| {
993                        endpoint_coord(primitives, candidate)
994                            .haversine_m(endpoint_coord(primitives, other))
995                    })
996                    .sum::<f64>()
997            };
998            score(lhs).total_cmp(&score(rhs)).then(lhs.cmp(&rhs))
999        })
1000        .expect("endpoint cluster is nonempty")
1001}
1002
1003fn projected_snap(
1004    endpoint: Coord,
1005    target_a: Coord,
1006    target_b: Coord,
1007    tolerance_m2: f64,
1008) -> Option<(f64, Coord, f64)> {
1009    let longitude_scale = 111_320.0 * endpoint.lat.to_radians().cos();
1010    let latitude_scale = 110_540.0;
1011    let vx = (target_b.lon - target_a.lon) * longitude_scale;
1012    let vy = (target_b.lat - target_a.lat) * latitude_scale;
1013    let len2 = vx.mul_add(vx, vy * vy);
1014    if len2 <= f64::EPSILON {
1015        return None;
1016    }
1017    let wx = (endpoint.lon - target_a.lon) * longitude_scale;
1018    let wy = (endpoint.lat - target_a.lat) * latitude_scale;
1019    let t = (wx.mul_add(vx, wy * vy) / len2).clamp(0.0, 1.0);
1020    let coord = target_a.lerp(target_b, t);
1021    let dx = wx - t * vx;
1022    let dy = wy - t * vy;
1023    let distance_m2 = dx.mul_add(dx, dy * dy);
1024    (distance_m2 <= tolerance_m2).then_some((t, coord, distance_m2))
1025}
1026
1027fn vertex_id(
1028    coord: Coord,
1029    source: Option<JunctionKey>,
1030    vertices: &mut Vec<Vertex>,
1031    vertex_by_key: &mut BTreeMap<VertexKey, VertexId>,
1032) -> VertexId {
1033    let key = source.map_or_else(
1034        || VertexKey::Coordinate(coord.lon.to_bits(), coord.lat.to_bits()),
1035        VertexKey::Source,
1036    );
1037    match vertex_by_key.entry(key) {
1038        Entry::Occupied(entry) => *entry.get(),
1039        Entry::Vacant(entry) => {
1040            let id = VertexId(vertices.len());
1041            let junction = match entry.key() {
1042                VertexKey::Source(key) => Some(key.clone()),
1043                VertexKey::Coordinate(_, _) => None,
1044            };
1045            entry.insert(id);
1046            vertices.push(Vertex {
1047                id,
1048                coord,
1049                junction,
1050            });
1051            id
1052        }
1053    }
1054}
1055
1056fn segment_intersection(lhs: Primitive, rhs: Primitive) -> Option<(f64, f64, Coord)> {
1057    let origin = (lhs.a.lon, lhs.a.lat);
1058    let ray = (lhs.b.lon - lhs.a.lon, lhs.b.lat - lhs.a.lat);
1059    let obstacle = (rhs.a.lon, rhs.a.lat);
1060    let sweep = (rhs.b.lon - rhs.a.lon, rhs.b.lat - rhs.a.lat);
1061    let determinant = cross(ray, sweep);
1062    if determinant.abs() < 1.0e-14 {
1063        return None;
1064    }
1065    let delta = (obstacle.0 - origin.0, obstacle.1 - origin.1);
1066    let t = cross(delta, sweep) / determinant;
1067    let u = cross(delta, ray) / determinant;
1068    if !(-1.0e-9..=1.0 + 1.0e-9).contains(&t) || !(-1.0e-9..=1.0 + 1.0e-9).contains(&u) {
1069        return None;
1070    }
1071    let coord = lhs.a.lerp(lhs.b, t.clamp(0.0, 1.0));
1072    Some((t.clamp(0.0, 1.0), u.clamp(0.0, 1.0), coord))
1073}
1074
1075fn cross(a: (f64, f64), b: (f64, f64)) -> f64 {
1076    a.0.mul_add(b.1, -a.1 * b.0)
1077}