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 #[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 #[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}