1use crate::geo::{Coord, LineString};
2use crate::hiking::{EdgeTraversal, HikingModel, TraversalEstimate};
3use crate::{Result, TrailgenError};
4use rstar::{AABB, RTree, RTreeObject};
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::collections::{BTreeMap, BinaryHeap};
8use std::ops::AddAssign;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct VertexId(pub usize);
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct EdgeId(pub usize);
17
18#[derive(
21 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
22)]
23#[serde(rename_all = "kebab-case")]
24pub enum WayKind {
25 #[default]
26 Unknown,
27 Path,
28 Footway,
29 Sidewalk,
30 Crossing,
31 Track,
32 #[serde(alias = "service")]
33 ServiceRoad,
34 PedestrianStreet,
35 Steps,
36 Bridleway,
37 Bushwhack,
40 #[serde(alias = "road")]
41 Roadway,
42 Cycleway,
45}
46
47#[derive(
50 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
51)]
52#[serde(rename_all = "kebab-case")]
53pub enum WayRealm {
54 #[default]
55 Recreational,
56 Connector,
57 Urban,
58}
59
60impl WayRealm {
61 #[must_use]
62 pub fn from_tag(tag: &str) -> Self {
63 match tag.trim().to_ascii_lowercase().as_str() {
64 "connector" | "recreational-connector" => Self::Connector,
65 "urban" | "pedestrian" | "circulation" => Self::Urban,
66 _ => Self::Recreational,
67 }
68 }
69
70 #[must_use]
71 pub const fn admitted_by_finder(self) -> bool {
72 matches!(self, Self::Recreational | Self::Connector)
73 }
74}
75
76#[derive(
77 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
78)]
79#[serde(rename_all = "kebab-case")]
80pub enum GeometryClaim {
81 #[default]
82 Surveyed,
83 CenterlineProxy,
84}
85
86impl GeometryClaim {
87 #[must_use]
88 pub fn from_tag(tag: &str) -> Self {
89 match tag.trim().to_ascii_lowercase().as_str() {
90 "centerline-proxy" | "road-centerline-proxy" | "proxy" => Self::CenterlineProxy,
91 _ => Self::Surveyed,
92 }
93 }
94}
95
96#[derive(
97 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
98)]
99#[serde(rename_all = "kebab-case")]
100pub enum CrossingControl {
101 #[default]
102 None,
103 Uncontrolled,
104 Marked,
105 Signals,
106 GradeSeparated,
107}
108
109impl CrossingControl {
110 #[must_use]
111 pub fn from_tag(tag: &str) -> Self {
112 match tag.trim().to_ascii_lowercase().as_str() {
113 "uncontrolled" | "unmarked" | "no" => Self::Uncontrolled,
114 "marked" | "zebra" | "uncontrolled-marked" => Self::Marked,
115 "signals" | "signal" | "traffic-signals" => Self::Signals,
116 "grade-separated" | "bridge" | "tunnel" => Self::GradeSeparated,
117 _ => Self::None,
118 }
119 }
120}
121
122#[derive(
126 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
127)]
128#[serde(rename_all = "kebab-case")]
129pub enum TrailStanding {
130 #[default]
131 Unknown,
132 Established,
133 Unmaintained,
134 Informal,
135 Historical,
136}
137
138#[derive(
141 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
142)]
143#[serde(rename_all = "kebab-case")]
144pub enum TrailMarking {
145 #[default]
146 Unknown,
147 Marked,
148 Unmarked,
149}
150
151impl TrailMarking {
152 #[must_use]
153 pub fn from_tag(tag: &str) -> Self {
154 match tag.trim().to_ascii_lowercase().as_str() {
155 "marked" | "marked trail" | "blazed" | "yes" | "symbols" | "poles" | "cairns" => {
156 Self::Marked
157 }
158 "unmarked" | "unmarked trail" | "unblazed" | "no" | "none" => Self::Unmarked,
159 _ => Self::Unknown,
160 }
161 }
162}
163
164impl TrailStanding {
165 #[must_use]
166 pub fn from_tag(tag: &str) -> Self {
167 match tag.trim().to_ascii_lowercase().as_str() {
168 "established" | "maintained" | "official" | "current" => Self::Established,
169 "unmaintained" | "disused" | "overgrown" => Self::Unmaintained,
170 "informal" | "social" | "social-trail" | "desire-path" => Self::Informal,
171 "historical" | "abandoned" | "removed" => Self::Historical,
172 _ => Self::Unknown,
173 }
174 }
175}
176
177impl WayKind {
178 #[must_use]
179 pub fn from_tag(tag: &str) -> Self {
180 match tag.trim().to_ascii_lowercase().as_str() {
181 "path" | "trail" | "singletrack" => Self::Path,
182 "footway" => Self::Footway,
183 "sidewalk" => Self::Sidewalk,
184 "crossing" => Self::Crossing,
185 "track" => Self::Track,
186 "service" | "service-road" => Self::ServiceRoad,
187 "pedestrian" | "pedestrian-way" => Self::PedestrianStreet,
188 "steps" | "stairs" => Self::Steps,
189 "bridleway" => Self::Bridleway,
190 "bushwhack" | "bushwhacking" | "off-trail" | "offtrail" | "cross-country" => {
191 Self::Bushwhack
192 }
193 "road" | "living_street" | "residential" | "unclassified" | "tertiary"
194 | "secondary" | "primary" => Self::Roadway,
195 "cycleway" | "cycle-way" => Self::Cycleway,
196 _ => Self::Unknown,
197 }
198 }
199
200 #[must_use]
201 pub const fn road_like(self) -> bool {
202 matches!(self, Self::Track | Self::ServiceRoad | Self::Roadway)
203 }
204
205 #[must_use]
206 pub const fn pathless(self) -> bool {
207 matches!(self, Self::Bushwhack)
208 }
209}
210
211#[derive(
212 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
213)]
214#[serde(rename_all = "kebab-case")]
215pub enum Terrain {
216 #[default]
217 Unknown,
218 Trail,
219 Forest,
220 Alpine,
221 Talus,
222 Scramble,
223 Pavement,
224 Road,
225 Water,
226}
227
228impl Terrain {
229 #[must_use]
230 pub fn from_tag(tag: &str) -> Self {
231 match tag.trim().to_ascii_lowercase().as_str() {
232 "trail" | "path" | "singletrack" => Self::Trail,
233 "forest" | "woods" => Self::Forest,
234 "alpine" | "tundra" => Self::Alpine,
235 "talus" | "scree" | "boulder" => Self::Talus,
236 "scramble" | "technical" => Self::Scramble,
237 "pavement" | "paved" | "asphalt" => Self::Pavement,
238 "road" | "service-road" | "gravel-road" => Self::Road,
239 "water" | "ford" => Self::Water,
240 _ => Self::Unknown,
241 }
242 }
243
244 #[must_use]
245 pub fn from_landcover_tag(tag: &str) -> Self {
246 let terrain = Self::from_tag(tag);
247 if terrain != Self::Unknown {
248 return terrain;
249 }
250 if let Ok(code) = tag.trim().parse::<u16>() {
251 return Self::from_nlcd_code(code);
252 }
253 match canonical_tag(tag).as_str() {
254 "openwater" | "emergentherbaceouswetlands" | "wetlands" => Self::Water,
255 "perennialicesnow"
256 | "shrubscrub"
257 | "grasslandherbaceous"
258 | "sedgeherbaceous"
259 | "lichens"
260 | "moss"
261 | "pasturehay"
262 | "cultivatedcrops" => Self::Alpine,
263 "developedopenspace"
264 | "developedlowintensity"
265 | "developedmediumintensity"
266 | "developedhighintensity"
267 | "developed"
268 | "urban" => Self::Pavement,
269 "barrenland" | "rocksandclay" | "barren" | "bedrock" | "rock" => Self::Talus,
270 "deciduousforest" | "evergreenforest" | "mixedforest" | "forestland"
271 | "woodywetlands" => Self::Forest,
272 _ => Self::Unknown,
273 }
274 }
275
276 const fn from_nlcd_code(code: u16) -> Self {
277 match code {
278 11 | 95 => Self::Water,
279 12 | 52 | 71 | 72 | 73 | 74 | 81 | 82 => Self::Alpine,
280 21..=24 => Self::Pavement,
281 31 => Self::Talus,
282 41..=43 | 90 => Self::Forest,
283 _ => Self::Unknown,
284 }
285 }
286}
287
288fn canonical_tag(tag: &str) -> String {
289 tag.chars()
290 .filter(char::is_ascii_alphanumeric)
291 .flat_map(char::to_lowercase)
292 .collect()
293}
294
295#[derive(
296 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
297)]
298#[serde(rename_all = "kebab-case")]
299pub enum Access {
300 #[default]
301 Unknown,
302 Open,
303 Restricted,
304 Closed,
305 Private,
306}
307
308impl Access {
309 #[must_use]
310 pub fn from_tag(tag: &str) -> Self {
311 match tag.trim().to_ascii_lowercase().as_str() {
312 "open" | "yes" | "permissive" => Self::Open,
313 "restricted" | "permit" => Self::Restricted,
314 "closed" | "no" => Self::Closed,
315 "private" => Self::Private,
316 _ => Self::Unknown,
317 }
318 }
319}
320
321#[derive(
322 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
323)]
324#[serde(rename_all = "kebab-case")]
325pub enum EdgeTravel {
326 #[default]
327 Both,
328 Forward,
329 Backward,
330}
331
332impl EdgeTravel {
333 #[must_use]
334 pub fn from_tag(tag: &str) -> Self {
335 match tag.trim().to_ascii_lowercase().as_str() {
336 "both" | "bidirectional" | "two-way" | "twoway" | "2" | "no" | "false" | "0" => {
337 Self::Both
338 }
339 "forward" | "with" | "yes" | "true" | "1" => Self::Forward,
340 "backward" | "reverse" | "against" | "-1" => Self::Backward,
341 _ => Self::Both,
342 }
343 }
344
345 #[must_use]
346 pub const fn can_depart(self, from: VertexId, a: VertexId, b: VertexId) -> bool {
347 match self {
348 Self::Both => from.0 == a.0 || from.0 == b.0,
349 Self::Forward => from.0 == a.0,
350 Self::Backward => from.0 == b.0,
351 }
352 }
353}
354
355#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
356pub struct Provenance {
357 pub source: String,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub layer: Option<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub source_id: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub license: Option<String>,
364}
365
366impl Provenance {
367 #[must_use]
368 pub fn fixture(source_id: impl Into<String>) -> Self {
369 Self {
370 source: "fixture".to_owned(),
371 layer: Some("mini-network".to_owned()),
372 source_id: Some(source_id.into()),
373 license: Some("CC0-fixture".to_owned()),
374 }
375 }
376}
377
378#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
379pub struct Vertex {
380 pub id: VertexId,
381 pub coord: crate::geo::Coord,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub junction: Option<crate::JunctionKey>,
384}
385
386#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
387pub struct TerrainEvidence {
388 pub terrain: Terrain,
389 pub confidence: f64,
390 pub rationale: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub provenance: Option<Provenance>,
393}
394
395#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
396#[serde(rename_all = "kebab-case")]
397pub enum CrossingKind {
398 Road,
399 Water,
400}
401
402impl CrossingKind {
403 #[must_use]
404 pub fn from_tag(tag: &str) -> Option<Self> {
405 match tag.trim().to_ascii_lowercase().as_str() {
406 "road" | "roads" | "pavement" | "highway" | "street" => Some(Self::Road),
407 "water" | "hydrology" | "stream" | "creek" | "river" | "ford" => Some(Self::Water),
408 _ => None,
409 }
410 }
411}
412
413#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
414pub struct CrossingEvidence {
415 pub kind: CrossingKind,
416 pub count: u32,
417 pub provenance: Provenance,
418}
419
420#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
421pub struct TurnBan {
422 pub via: VertexId,
423 pub from: EdgeId,
424 pub to: EdgeId,
425 pub provenance: Provenance,
426}
427
428#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
429pub struct GradeDistribution {
430 pub flat_m: f64,
431 pub rolling_m: f64,
432 pub steep_m: f64,
433 pub savage_m: f64,
434}
435
436impl GradeDistribution {
437 #[must_use]
438 pub fn add_segment(mut self, length_m: f64, abs_grade: f64) -> Self {
439 if abs_grade < 0.05 {
440 self.flat_m += length_m;
441 } else if abs_grade < 0.15 {
442 self.rolling_m += length_m;
443 } else if abs_grade < 0.30 {
444 self.steep_m += length_m;
445 } else {
446 self.savage_m += length_m;
447 }
448 self
449 }
450
451 #[must_use]
452 pub fn total_m(self) -> f64 {
453 self.flat_m + self.rolling_m + self.steep_m + self.savage_m
454 }
455}
456
457impl AddAssign for GradeDistribution {
458 fn add_assign(&mut self, rhs: Self) {
459 self.flat_m += rhs.flat_m;
460 self.rolling_m += rhs.rolling_m;
461 self.steep_m += rhs.steep_m;
462 self.savage_m += rhs.savage_m;
463 }
464}
465
466#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
467pub struct EdgeAttr {
468 pub length_m: f64,
469 pub ascent_m: f64,
470 pub descent_m: f64,
471 pub grade_abs_mean: f64,
472 #[serde(default)]
473 pub grade_abs_max: f64,
474 #[serde(default)]
475 pub sustained_steep_m: f64,
476 #[serde(default)]
477 pub grade_distribution: GradeDistribution,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub hill_slope_deg: Option<f64>,
482 #[serde(default)]
483 pub way_kind: WayKind,
484 #[serde(default)]
485 pub realm: WayRealm,
486 #[serde(default)]
487 pub geometry_claim: GeometryClaim,
488 #[serde(default)]
489 pub crossing_control: CrossingControl,
490 #[serde(default)]
491 pub standing: TrailStanding,
492 #[serde(default)]
493 pub marking: TrailMarking,
494 pub terrain: Terrain,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub surface: Option<String>,
497 #[serde(default)]
498 pub terrain_confidence: f64,
499 #[serde(default)]
500 pub terrain_evidence: Vec<TerrainEvidence>,
501 pub access: Access,
502 #[serde(default)]
503 pub travel: EdgeTravel,
504 #[serde(default)]
505 pub access_confidence: f64,
506 #[serde(default)]
507 pub access_provenance: Vec<Provenance>,
508 #[serde(default)]
509 pub crossings: Vec<CrossingEvidence>,
510 pub road_exposure: f64,
511 pub confidence: f64,
512 #[serde(default)]
513 pub traversal: EdgeTraversal,
514 #[serde(default)]
515 pub seed_count: u32,
516 #[serde(default)]
517 pub popularity: f64,
518 #[serde(default)]
519 pub seed_provenance: Vec<Provenance>,
520 #[serde(default)]
521 pub elevation_provenance: Vec<Provenance>,
522 pub provenance: Vec<Provenance>,
523}
524
525#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
526pub struct Edge {
527 pub id: EdgeId,
528 pub a: VertexId,
529 pub b: VertexId,
530 pub geometry: LineString,
531 pub attr: EdgeAttr,
532}
533
534#[derive(Clone, Copy, Debug, PartialEq)]
535pub struct EdgeProjection {
536 pub edge: EdgeId,
537 pub coord: Coord,
538 pub progress_m: f64,
539 pub distance_m: f64,
540}
541
542impl Edge {
543 #[must_use]
544 pub fn other(&self, v: VertexId) -> Option<VertexId> {
545 if v == self.a {
546 Some(self.b)
547 } else if v == self.b {
548 Some(self.a)
549 } else {
550 None
551 }
552 }
553
554 #[must_use]
555 pub fn traverse(&self, from: VertexId) -> Option<VertexId> {
556 if !self.attr.travel.can_depart(from, self.a, self.b) {
557 return None;
558 }
559 self.other(from)
560 }
561
562 #[must_use]
563 pub fn oriented_geometry(&self, from: VertexId) -> LineString {
564 if from == self.a {
565 self.geometry.clone()
566 } else {
567 self.geometry.reversed()
568 }
569 }
570
571 #[must_use]
572 pub const fn traversal_from(&self, from: VertexId) -> TraversalEstimate {
573 self.attr.traversal.departing(self, from)
574 }
575}
576
577#[derive(Clone, Debug, PartialEq, Serialize)]
578pub struct WalkGraph {
579 pub vertices: Vec<Vertex>,
580 pub edges: Vec<Edge>,
581 #[serde(default)]
582 pub turn_bans: Vec<TurnBan>,
583 #[serde(skip_serializing)]
584 pub adjacency: Vec<Vec<EdgeId>>,
585}
586
587impl<'de> Deserialize<'de> for WalkGraph {
588 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
589 where
590 D: serde::Deserializer<'de>,
591 {
592 #[derive(Deserialize)]
593 struct StoredGraph {
594 vertices: Vec<Vertex>,
595 edges: Vec<Edge>,
596 #[serde(default)]
597 turn_bans: Vec<TurnBan>,
598 }
599
600 let stored = StoredGraph::deserialize(deserializer)?;
601 let mut graph = Self {
602 vertices: stored.vertices,
603 edges: stored.edges,
604 turn_bans: stored.turn_bans,
605 adjacency: Vec::new(),
606 };
607 for edge in &mut graph.edges {
608 HikingModel.apply(edge);
609 }
610 graph.validate().map_err(serde::de::Error::custom)?;
611 graph.rebuild_adjacency();
612 Ok(graph)
613 }
614}
615
616#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
617pub struct RouteSnapStats {
618 pub segment_count: usize,
619 pub snapped_segment_count: usize,
620 pub rejected_segment_count: usize,
621 #[serde(default)]
622 pub disconnected_transition_count: usize,
623 pub max_snap_m: f64,
624 pub mean_snap_m: f64,
625}
626
627#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
628#[serde(rename_all = "kebab-case")]
629pub enum CoverageGapKind {
630 BeyondNetwork,
631 DisconnectedNetwork,
632}
633
634#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
635pub struct CoverageGap {
636 pub kind: CoverageGapKind,
637 pub first_route_segment: usize,
638 pub last_route_segment: usize,
639 pub geometry: LineString,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub nearest_edge: Option<EdgeId>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub nearest_distance_m: Option<f64>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub before_edge: Option<EdgeId>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub after_edge: Option<EdgeId>,
648}
649
650#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
651pub struct RouteCoverage {
652 pub edges: Vec<EdgeId>,
653 pub stats: RouteSnapStats,
654 pub gaps: Vec<CoverageGap>,
655}
656
657#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
658struct WalkState {
659 at: VertexId,
660 previous: Option<EdgeId>,
661}
662
663#[derive(Clone, Copy, Debug, PartialEq)]
664struct WalkFrontier {
665 cost_m: f64,
666 state: WalkState,
667}
668
669#[derive(Clone, Copy, Debug)]
670struct SnapAnchor {
671 edge: EdgeId,
672 entry: VertexId,
673 budget_m: f64,
674 route_segment: usize,
675}
676
677#[derive(Clone, Copy, Debug)]
678struct DisconnectedSpan {
679 from_segment: usize,
680 to_segment: usize,
681 before: EdgeId,
682 after: EdgeId,
683}
684
685#[derive(Clone, Copy)]
686struct EdgeEnvelope {
687 edge: EdgeId,
688 bounds: AABB<[f64; 2]>,
689}
690
691#[derive(Clone)]
692pub struct EdgeIndex {
693 tree: RTree<EdgeEnvelope>,
694}
695
696impl EdgeIndex {
697 #[must_use]
698 pub fn forge(graph: &WalkGraph) -> Self {
699 Self {
700 tree: edge_spatial_index(graph.edges.iter()),
701 }
702 }
703
704 #[must_use]
705 pub fn forge_allowed(graph: &WalkGraph, allowed: &[bool]) -> Self {
706 assert_eq!(allowed.len(), graph.edges.len());
707 Self {
708 tree: edge_spatial_index(
709 graph
710 .edges
711 .iter()
712 .zip(allowed)
713 .filter_map(|(edge, allowed)| allowed.then_some(edge)),
714 ),
715 }
716 }
717
718 #[must_use]
719 pub fn project(&self, graph: &WalkGraph, coord: Coord) -> Option<EdgeProjection> {
720 let (edge, distance_m) = indexed_nearest_edge(&graph.edges, &self.tree, coord)?;
721 let (_, progress_m, coord) = line_projection(&graph.edges[edge.0].geometry, coord)?;
722 Some(EdgeProjection {
723 edge,
724 coord,
725 progress_m,
726 distance_m,
727 })
728 }
729
730 #[must_use]
734 pub fn candidates(
735 &self,
736 graph: &WalkGraph,
737 coord: Coord,
738 max_distance_m: f64,
739 limit: usize,
740 ) -> Vec<EdgeProjection> {
741 if !max_distance_m.is_finite() || max_distance_m < 0.0 || limit == 0 {
742 return Vec::new();
743 }
744 let latitude_radius = max_distance_m / 110_540.0;
745 let longitude_radius =
746 max_distance_m / (111_320.0 * coord.lat.to_radians().cos().abs().max(0.01));
747 let neighborhood = AABB::from_corners(
748 [coord.lon - longitude_radius, coord.lat - latitude_radius],
749 [coord.lon + longitude_radius, coord.lat + latitude_radius],
750 );
751 let mut candidates = self
752 .tree
753 .locate_in_envelope_intersecting(neighborhood)
754 .filter_map(|candidate| {
755 let (distance_m, progress_m, anchor) =
756 line_projection(&graph.edges[candidate.edge.0].geometry, coord)?;
757 (distance_m <= max_distance_m).then_some(EdgeProjection {
758 edge: candidate.edge,
759 coord: anchor,
760 progress_m,
761 distance_m,
762 })
763 })
764 .collect::<Vec<_>>();
765 candidates.sort_by(|left, right| {
766 left.distance_m
767 .total_cmp(&right.distance_m)
768 .then_with(|| {
769 edge_anchor_rank(&graph.edges[left.edge.0])
770 .cmp(&edge_anchor_rank(&graph.edges[right.edge.0]))
771 })
772 .then_with(|| left.edge.cmp(&right.edge))
773 .then_with(|| left.progress_m.total_cmp(&right.progress_m))
774 });
775 candidates.truncate(limit);
776 candidates
777 }
778}
779
780impl RTreeObject for EdgeEnvelope {
781 type Envelope = AABB<[f64; 2]>;
782
783 fn envelope(&self) -> Self::Envelope {
784 self.bounds
785 }
786}
787
788impl Eq for WalkFrontier {}
789
790impl Ord for WalkFrontier {
791 fn cmp(&self, other: &Self) -> Ordering {
792 other
793 .cost_m
794 .total_cmp(&self.cost_m)
795 .then_with(|| other.state.cmp(&self.state))
796 }
797}
798
799impl PartialOrd for WalkFrontier {
800 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
801 Some(self.cmp(other))
802 }
803}
804
805impl WalkGraph {
806 #[must_use]
807 pub fn new(vertices: Vec<Vertex>, edges: Vec<Edge>) -> Self {
808 let mut graph = Self {
809 adjacency: vec![Vec::new(); vertices.len()],
810 vertices,
811 edges,
812 turn_bans: Vec::new(),
813 };
814 graph.rebuild_adjacency();
815 graph
816 }
817
818 pub fn rebuild_adjacency(&mut self) {
819 self.adjacency.clear();
820 self.adjacency.resize_with(self.vertices.len(), Vec::new);
821 for edge in &self.edges {
822 if edge.attr.travel.can_depart(edge.a, edge.a, edge.b) {
823 self.adjacency[edge.a.0].push(edge.id);
824 }
825 if edge.attr.travel.can_depart(edge.b, edge.a, edge.b) {
826 self.adjacency[edge.b.0].push(edge.id);
827 }
828 }
829 }
830
831 pub fn validate(&self) -> Result<()> {
832 for (index, vertex) in self.vertices.iter().enumerate() {
833 if vertex.id.0 != index {
834 return Err(TrailgenError::InvalidData(format!(
835 "vertex slot {index} contains id {}",
836 vertex.id.0
837 )));
838 }
839 if !valid_coord(vertex.coord) {
840 return Err(TrailgenError::InvalidData(format!(
841 "vertex {index} has invalid coordinate"
842 )));
843 }
844 }
845 for (index, edge) in self.edges.iter().enumerate() {
846 if edge.id.0 != index {
847 return Err(TrailgenError::InvalidData(format!(
848 "edge slot {index} contains id {}",
849 edge.id.0
850 )));
851 }
852 if edge.a == edge.b
853 || edge.a.0 >= self.vertices.len()
854 || edge.b.0 >= self.vertices.len()
855 {
856 return Err(TrailgenError::InvalidData(format!(
857 "edge {index} has invalid endpoints {} and {}",
858 edge.a.0, edge.b.0
859 )));
860 }
861 if edge.geometry.points.len() < 2
862 || edge
863 .geometry
864 .points
865 .iter()
866 .copied()
867 .any(|coord| !valid_coord(coord))
868 || !edge.attr.length_m.is_finite()
869 || edge.attr.length_m <= 0.0
870 || !edge.attr.traversal.valid()
871 {
872 return Err(TrailgenError::InvalidData(format!(
873 "edge {index} has invalid geometry or length"
874 )));
875 }
876 }
877 for ban in &self.turn_bans {
878 if ban.via.0 >= self.vertices.len()
879 || ban.from.0 >= self.edges.len()
880 || ban.to.0 >= self.edges.len()
881 || self.edges[ban.from.0]
882 .other(ban.via)
883 .is_none_or(|_| self.edges[ban.to.0].other(ban.via).is_none())
884 {
885 return Err(TrailgenError::InvalidData(format!(
886 "turn ban e{}→v{}→e{} does not reference incident graph members",
887 ban.from.0, ban.via.0, ban.to.0
888 )));
889 }
890 }
891 Ok(())
892 }
893
894 #[must_use]
895 pub fn nearest_vertex(&self, coord: crate::geo::Coord) -> Option<VertexId> {
896 self.nearest_vertex_with_distance(coord)
897 .map(|(vertex, _)| vertex)
898 }
899
900 #[must_use]
901 pub fn nearest_vertex_with_distance(
902 &self,
903 coord: crate::geo::Coord,
904 ) -> Option<(VertexId, f64)> {
905 self.vertices
906 .iter()
907 .min_by(|a, b| {
908 a.coord
909 .planar_distance2(coord)
910 .total_cmp(&b.coord.planar_distance2(coord))
911 })
912 .map(|v| (v.id, v.coord.haversine_m(coord)))
913 }
914
915 #[must_use]
916 pub fn snap_line_edges(&self, line: &crate::geo::LineString) -> Vec<EdgeId> {
917 self.trace_coverage(line, f64::INFINITY).edges
918 }
919
920 #[must_use]
921 pub fn trace_coverage(&self, line: &crate::geo::LineString, max_snap_m: f64) -> RouteCoverage {
922 let edge_index = edge_spatial_index(self.edges.iter());
923 let mut anchors = Vec::new();
924 let mut gaps = Vec::new();
925 let mut segment_count = 0usize;
926 let mut snapped_segment_count = 0usize;
927 let mut rejected_segment_count = 0usize;
928 let mut max_observed_m = 0.0f64;
929 let mut sum_observed_m = 0.0f64;
930 let mut observed_count = 0.0f64;
931 for (route_segment, w) in line.points.windows(2).enumerate() {
932 segment_count += 1;
933 let mid = w[0].lerp(w[1], 0.5);
934 let Some((edge_id, snap_m)) = indexed_nearest_edge(&self.edges, &edge_index, mid)
935 else {
936 rejected_segment_count += 1;
937 gaps.push(CoverageGap {
938 kind: CoverageGapKind::BeyondNetwork,
939 first_route_segment: route_segment,
940 last_route_segment: route_segment,
941 geometry: LineString::unchecked(w.to_vec()),
942 nearest_edge: None,
943 nearest_distance_m: None,
944 before_edge: None,
945 after_edge: None,
946 });
947 continue;
948 };
949 max_observed_m = max_observed_m.max(snap_m);
950 sum_observed_m += snap_m;
951 observed_count += 1.0;
952 if snap_m > max_snap_m {
953 rejected_segment_count += 1;
954 gaps.push(CoverageGap {
955 kind: CoverageGapKind::BeyondNetwork,
956 first_route_segment: route_segment,
957 last_route_segment: route_segment,
958 geometry: LineString::unchecked(w.to_vec()),
959 nearest_edge: Some(edge_id),
960 nearest_distance_m: Some(snap_m),
961 before_edge: None,
962 after_edge: None,
963 });
964 continue;
965 }
966 snapped_segment_count += 1;
967 if anchors
968 .last()
969 .is_none_or(|last: &SnapAnchor| last.edge != edge_id)
970 {
971 let observation_m = w[0].haversine_m(w[1]);
972 let snap_allowance_m = if max_snap_m.is_finite() {
973 max_snap_m * 2.0
974 } else {
975 500.0
976 };
977 let edge = &self.edges[edge_id.0];
978 let start_progress_m = line_progress_m(&edge.geometry, w[0]);
979 let end_progress_m = line_progress_m(&edge.geometry, w[1]);
980 anchors.push(SnapAnchor {
981 edge: edge_id,
982 entry: if end_progress_m >= start_progress_m {
983 edge.a
984 } else {
985 edge.b
986 },
987 budget_m: observation_m.mul_add(3.0, snap_allowance_m).max(50.0),
988 route_segment,
989 });
990 }
991 }
992 self.collapse_shadow_anchors(&mut anchors, line, max_snap_m);
993 let (edges, disconnected) = self.connect_snap_anchors(&anchors);
994 gaps.extend(disconnected.iter().map(|span| CoverageGap {
995 kind: CoverageGapKind::DisconnectedNetwork,
996 first_route_segment: span.from_segment.min(span.to_segment),
997 last_route_segment: span.from_segment.max(span.to_segment),
998 geometry: coverage_geometry(
999 line,
1000 span.from_segment.min(span.to_segment),
1001 span.from_segment.max(span.to_segment),
1002 ),
1003 nearest_edge: None,
1004 nearest_distance_m: None,
1005 before_edge: Some(span.before),
1006 after_edge: Some(span.after),
1007 }));
1008 RouteCoverage {
1009 edges,
1010 stats: coverage_stats(
1011 segment_count,
1012 snapped_segment_count,
1013 rejected_segment_count,
1014 disconnected.len(),
1015 max_observed_m,
1016 sum_observed_m,
1017 observed_count,
1018 ),
1019 gaps: coalesce_coverage_gaps(gaps, line),
1020 }
1021 }
1022
1023 fn collapse_shadow_anchors(
1024 &self,
1025 anchors: &mut Vec<SnapAnchor>,
1026 line: &crate::geo::LineString,
1027 max_snap_m: f64,
1028 ) {
1029 if !max_snap_m.is_finite() {
1030 return;
1031 }
1032 let mut first = 0;
1033 while first + 2 < anchors.len() {
1034 let shadowed = ((first + 2)..anchors.len()).rev().find(|&last| {
1035 anchors[first].edge == anchors[last].edge
1036 && anchors[first].entry == anchors[last].entry
1037 && route_span_fits_edge(
1038 &self.edges[anchors[first].edge.0],
1039 line,
1040 anchors[first].route_segment,
1041 anchors[last].route_segment,
1042 max_snap_m,
1043 )
1044 });
1045 if let Some(last) = shadowed {
1046 anchors.drain(first + 1..=last);
1047 } else {
1048 first += 1;
1049 }
1050 }
1051 }
1052
1053 fn connect_snap_anchors(&self, anchors: &[SnapAnchor]) -> (Vec<EdgeId>, Vec<DisconnectedSpan>) {
1054 let Some(first_anchor) = anchors.first().copied() else {
1055 return (Vec::new(), Vec::new());
1056 };
1057 let first_id = first_anchor.edge;
1058 let Some(first) = self.edges.get(first_id.0) else {
1059 return (
1060 Vec::new(),
1061 vec![DisconnectedSpan {
1062 from_segment: first_anchor.route_segment,
1063 to_segment: first_anchor.route_segment,
1064 before: first_id,
1065 after: first_id,
1066 }],
1067 );
1068 };
1069 let start = first_anchor.entry;
1070 if first.traverse(start).is_none() {
1071 return (
1072 Vec::new(),
1073 vec![DisconnectedSpan {
1074 from_segment: first_anchor.route_segment,
1075 to_segment: first_anchor.route_segment,
1076 before: first_id,
1077 after: first_id,
1078 }],
1079 );
1080 }
1081 let mut edges = vec![first_id];
1082 let mut at = first
1083 .traverse(start)
1084 .expect("start was filtered as traversable");
1085 let mut previous = first_id;
1086 let mut previous_anchor = first_anchor;
1087 let mut disconnected = Vec::new();
1088 for anchor in &anchors[1..] {
1089 let target = anchor.edge;
1090 if at == anchor.entry
1091 && self.turn_allowed(Some(previous), at, target)
1092 && let Some(next) = self.edges[target.0].traverse(at)
1093 {
1094 edges.push(target);
1095 previous = target;
1096 at = next;
1097 previous_anchor = *anchor;
1098 continue;
1099 }
1100 let Some(connector) =
1101 self.shortest_connector(at, previous, target, anchor.entry, anchor.budget_m)
1102 else {
1103 self.record_disconnection(&mut disconnected, previous_anchor, *anchor);
1104 if let Some(next) = self.edges[target.0].traverse(anchor.entry) {
1105 edges.clear();
1106 edges.push(target);
1107 at = next;
1108 previous = target;
1109 previous_anchor = *anchor;
1110 continue;
1111 }
1112 previous_anchor = *anchor;
1113 continue;
1114 };
1115 for edge_id in connector {
1116 at = self.edges[edge_id.0]
1117 .traverse(at)
1118 .expect("connector reconstruction preserves direction");
1119 previous = edge_id;
1120 edges.push(edge_id);
1121 }
1122 if at == anchor.entry
1123 && self.turn_allowed(Some(previous), at, target)
1124 && let Some(next) = self.edges[target.0].traverse(at)
1125 {
1126 edges.push(target);
1127 previous = target;
1128 at = next;
1129 } else {
1130 self.record_disconnection(&mut disconnected, previous_anchor, *anchor);
1131 }
1132 previous_anchor = *anchor;
1133 }
1134 (edges, disconnected)
1135 }
1136
1137 fn record_disconnection(
1138 &self,
1139 disconnected: &mut Vec<DisconnectedSpan>,
1140 before: SnapAnchor,
1141 after: SnapAnchor,
1142 ) {
1143 if !self.edges_connect_locally(before.edge, after.edge, before.budget_m.max(after.budget_m))
1144 {
1145 disconnected.push(DisconnectedSpan {
1146 from_segment: before.route_segment,
1147 to_segment: after.route_segment,
1148 before: before.edge,
1149 after: after.edge,
1150 });
1151 }
1152 }
1153
1154 fn edges_connect_locally(&self, before: EdgeId, after: EdgeId, budget_m: f64) -> bool {
1155 let before_edge = &self.edges[before.0];
1156 let after_edge = &self.edges[after.0];
1157 [before_edge.a, before_edge.b]
1158 .into_iter()
1159 .filter_map(|entry| before_edge.traverse(entry))
1160 .any(|at| {
1161 [after_edge.a, after_edge.b]
1162 .into_iter()
1163 .filter(|entry| after_edge.traverse(*entry).is_some())
1164 .any(|entry| {
1165 (at == entry && self.turn_allowed(Some(before), at, after))
1166 || self
1167 .shortest_connector(at, before, after, entry, budget_m)
1168 .is_some()
1169 })
1170 })
1171 }
1172
1173 fn shortest_connector(
1174 &self,
1175 from: VertexId,
1176 previous: EdgeId,
1177 target: EdgeId,
1178 target_entry: VertexId,
1179 max_distance_m: f64,
1180 ) -> Option<Vec<EdgeId>> {
1181 let origin = WalkState {
1182 at: from,
1183 previous: Some(previous),
1184 };
1185 let mut frontier = BinaryHeap::from([WalkFrontier {
1186 cost_m: 0.0,
1187 state: origin,
1188 }]);
1189 let mut distance = BTreeMap::from([(origin, 0.0)]);
1190 let mut predecessor = BTreeMap::<WalkState, (WalkState, EdgeId)>::new();
1191 while let Some(WalkFrontier { cost_m, state }) = frontier.pop() {
1192 if cost_m > max_distance_m
1193 || distance
1194 .get(&state)
1195 .is_some_and(|best| cost_m > *best + f64::EPSILON)
1196 {
1197 continue;
1198 }
1199 if state.at == target_entry
1200 && self.turn_allowed(state.previous, state.at, target)
1201 && self.edges[target.0].traverse(state.at).is_some()
1202 {
1203 let mut path = Vec::new();
1204 let mut cursor = state;
1205 while cursor != origin {
1206 let (prior, edge) = predecessor.get(&cursor).copied()?;
1207 path.push(edge);
1208 cursor = prior;
1209 }
1210 path.reverse();
1211 return Some(path);
1212 }
1213 for edge_id in self.adjacency.get(state.at.0)?.iter().copied() {
1214 if edge_id == target || !self.turn_allowed(state.previous, state.at, edge_id) {
1215 continue;
1216 }
1217 let edge = self.edges.get(edge_id.0)?;
1218 let Some(to) = edge.traverse(state.at) else {
1219 continue;
1220 };
1221 let next_cost = cost_m + edge.attr.length_m;
1222 if next_cost > max_distance_m {
1223 continue;
1224 }
1225 let next = WalkState {
1226 at: to,
1227 previous: Some(edge_id),
1228 };
1229 if distance.get(&next).is_none_or(|best| next_cost < *best) {
1230 distance.insert(next, next_cost);
1231 predecessor.insert(next, (state, edge_id));
1232 frontier.push(WalkFrontier {
1233 cost_m: next_cost,
1234 state: next,
1235 });
1236 }
1237 }
1238 }
1239 None
1240 }
1241
1242 #[must_use]
1243 pub fn nearest_edge(&self, coord: crate::geo::Coord) -> Option<EdgeId> {
1244 self.nearest_edge_with_distance(coord).map(|(edge, _)| edge)
1245 }
1246
1247 #[must_use]
1248 pub fn nearest_edge_with_distance(&self, coord: crate::geo::Coord) -> Option<(EdgeId, f64)> {
1249 self.project_onto_edge(coord)
1250 .map(|projection| (projection.edge, projection.distance_m))
1251 }
1252
1253 #[must_use]
1254 pub fn project_onto_edge(&self, coord: Coord) -> Option<EdgeProjection> {
1255 self.edges
1256 .iter()
1257 .filter_map(|edge| {
1258 line_projection(&edge.geometry, coord).map(|(distance_m, progress_m, coord)| {
1259 EdgeProjection {
1260 edge: edge.id,
1261 coord,
1262 progress_m,
1263 distance_m,
1264 }
1265 })
1266 })
1267 .min_by(|left, right| left.distance_m.total_cmp(&right.distance_m))
1268 }
1269
1270 #[must_use]
1271 pub fn snapped_line_start(
1272 &self,
1273 line: &crate::geo::LineString,
1274 edges: &[EdgeId],
1275 ) -> Option<VertexId> {
1276 let first = self.edges.get(edges.first()?.0)?;
1277 let line_start = line.start();
1278 [first.a, first.b]
1279 .into_iter()
1280 .filter(|start| self.walk_edges(*start, edges).is_some())
1281 .min_by(|a, b| {
1282 self.vertices[a.0]
1283 .coord
1284 .planar_distance2(line_start)
1285 .total_cmp(&self.vertices[b.0].coord.planar_distance2(line_start))
1286 })
1287 }
1288
1289 #[must_use]
1290 pub fn walk_edges(&self, start: VertexId, edges: &[EdgeId]) -> Option<VertexId> {
1291 let mut at = start;
1292 let mut previous = None;
1293 for edge_id in edges {
1294 if !self.turn_allowed(previous, at, *edge_id) {
1295 return None;
1296 }
1297 at = self.edges.get(edge_id.0)?.traverse(at)?;
1298 previous = Some(*edge_id);
1299 }
1300 Some(at)
1301 }
1302
1303 #[must_use]
1304 pub fn turn_allowed(&self, from: Option<EdgeId>, via: VertexId, to: EdgeId) -> bool {
1305 from.is_none_or(|from| {
1306 !self
1307 .turn_bans
1308 .iter()
1309 .any(|ban| ban.via == via && ban.from == from && ban.to == to)
1310 })
1311 }
1312
1313 pub fn apply_seed_hints(&mut self, seed: &crate::seed::SeedRoute) {
1314 for edge_id in &seed.snapped_edges {
1315 let Some(edge) = self.edges.get_mut(edge_id.0) else {
1316 continue;
1317 };
1318 if !edge.attr.seed_provenance.contains(&seed.provenance) {
1319 edge.attr.seed_count = edge.attr.seed_count.saturating_add(1);
1320 edge.attr.seed_provenance.push(seed.provenance.clone());
1321 }
1322 edge.attr.popularity = f64::from(edge.attr.seed_count).ln_1p();
1323 edge.attr.confidence = edge.attr.confidence.max(0.82);
1324 }
1325 }
1326}
1327
1328fn coverage_geometry(line: &LineString, first: usize, last: usize) -> LineString {
1329 let start = first.min(line.points.len().saturating_sub(2));
1330 let end = last
1331 .saturating_add(1)
1332 .min(line.points.len().saturating_sub(1));
1333 LineString::unchecked(line.points[start..=end.max(start + 1)].to_vec())
1334}
1335
1336fn coalesce_coverage_gaps(mut gaps: Vec<CoverageGap>, line: &LineString) -> Vec<CoverageGap> {
1337 gaps.sort_by_key(|gap| (gap.first_route_segment, gap.last_route_segment));
1338 let mut merged = Vec::<CoverageGap>::new();
1339 for gap in gaps {
1340 let Some(previous) = merged.last_mut() else {
1341 merged.push(gap);
1342 continue;
1343 };
1344 if previous.kind != gap.kind
1345 || gap.first_route_segment > previous.last_route_segment.saturating_add(1)
1346 {
1347 merged.push(gap);
1348 continue;
1349 }
1350 previous.last_route_segment = previous.last_route_segment.max(gap.last_route_segment);
1351 previous.geometry = coverage_geometry(
1352 line,
1353 previous.first_route_segment,
1354 previous.last_route_segment,
1355 );
1356 if previous.nearest_edge != gap.nearest_edge {
1357 previous.nearest_edge = None;
1358 }
1359 if previous.kind == CoverageGapKind::DisconnectedNetwork {
1360 previous.after_edge = gap.after_edge;
1361 } else {
1362 if previous.before_edge != gap.before_edge {
1363 previous.before_edge = None;
1364 }
1365 if previous.after_edge != gap.after_edge {
1366 previous.after_edge = None;
1367 }
1368 }
1369 previous.nearest_distance_m = match (previous.nearest_distance_m, gap.nearest_distance_m) {
1370 (Some(left), Some(right)) => Some(left.max(right)),
1371 _ => None,
1372 };
1373 }
1374 merged
1375}
1376
1377fn valid_coord(coord: Coord) -> bool {
1378 coord.lon.is_finite()
1379 && coord.lat.is_finite()
1380 && (-180.0..=180.0).contains(&coord.lon)
1381 && (-90.0..=90.0).contains(&coord.lat)
1382 && coord.ele.is_none_or(f64::is_finite)
1383}
1384
1385fn edge_distance_m(edge: &Edge, coord: Coord) -> f64 {
1386 line_projection(&edge.geometry, coord).map_or(f64::INFINITY, |projection| projection.0)
1387}
1388
1389fn edge_spatial_index<'a>(edges: impl IntoIterator<Item = &'a Edge>) -> RTree<EdgeEnvelope> {
1390 RTree::bulk_load(
1391 edges
1392 .into_iter()
1393 .map(|edge| {
1394 let (west, south, east, north) = edge.geometry.points.iter().fold(
1395 (
1396 f64::INFINITY,
1397 f64::INFINITY,
1398 f64::NEG_INFINITY,
1399 f64::NEG_INFINITY,
1400 ),
1401 |(west, south, east, north), point| {
1402 (
1403 west.min(point.lon),
1404 south.min(point.lat),
1405 east.max(point.lon),
1406 north.max(point.lat),
1407 )
1408 },
1409 );
1410 EdgeEnvelope {
1411 edge: edge.id,
1412 bounds: AABB::from_corners([west, south], [east, north]),
1413 }
1414 })
1415 .collect(),
1416 )
1417}
1418
1419fn coverage_stats(
1420 segment_count: usize,
1421 snapped_segment_count: usize,
1422 rejected_segment_count: usize,
1423 disconnected_transition_count: usize,
1424 max_snap_m: f64,
1425 sum_snap_m: f64,
1426 observed_count: f64,
1427) -> RouteSnapStats {
1428 RouteSnapStats {
1429 segment_count,
1430 snapped_segment_count,
1431 rejected_segment_count,
1432 disconnected_transition_count,
1433 max_snap_m,
1434 mean_snap_m: if observed_count <= f64::EPSILON {
1435 0.0
1436 } else {
1437 sum_snap_m / observed_count
1438 },
1439 }
1440}
1441
1442fn indexed_nearest_edge(
1443 edges: &[Edge],
1444 index: &RTree<EdgeEnvelope>,
1445 coord: Coord,
1446) -> Option<(EdgeId, f64)> {
1447 for radius_m in std::iter::successors(Some(32.0), |radius| Some(radius * 4.0)).take(11) {
1448 let latitude_radius = radius_m / 110_540.0;
1449 let longitude_radius =
1450 radius_m / (111_320.0 * coord.lat.to_radians().cos().abs().max(0.01));
1451 let neighborhood = AABB::from_corners(
1452 [coord.lon - longitude_radius, coord.lat - latitude_radius],
1453 [coord.lon + longitude_radius, coord.lat + latitude_radius],
1454 );
1455 let nearest = index
1456 .locate_in_envelope_intersecting(neighborhood)
1457 .map(|candidate| {
1458 let distance_m = edge_distance_m(&edges[candidate.edge.0], coord);
1459 (candidate.edge, distance_m)
1460 })
1461 .min_by(|left, right| {
1462 left.1
1463 .total_cmp(&right.1)
1464 .then_with(|| {
1465 edge_anchor_rank(&edges[left.0.0]).cmp(&edge_anchor_rank(&edges[right.0.0]))
1466 })
1467 .then_with(|| left.0.cmp(&right.0))
1468 });
1469 if nearest.is_some_and(|(_, distance_m)| distance_m <= radius_m) {
1470 return nearest;
1471 }
1472 }
1473 edges
1474 .iter()
1475 .map(|edge| (edge.id, edge_distance_m(edge, coord)))
1476 .min_by(|left, right| {
1477 left.1
1478 .total_cmp(&right.1)
1479 .then_with(|| {
1480 edge_anchor_rank(&edges[left.0.0]).cmp(&edge_anchor_rank(&edges[right.0.0]))
1481 })
1482 .then_with(|| left.0.cmp(&right.0))
1483 })
1484}
1485
1486const fn edge_anchor_rank(edge: &Edge) -> (u8, u8, u8) {
1487 let access = match edge.attr.access {
1488 Access::Open => 0,
1489 Access::Unknown | Access::Restricted => 1,
1490 Access::Closed | Access::Private => 2,
1491 };
1492 let geometry = match edge.attr.geometry_claim {
1493 GeometryClaim::Surveyed => 0,
1494 GeometryClaim::CenterlineProxy => 1,
1495 };
1496 let function = match edge.attr.way_kind {
1497 WayKind::Path
1498 | WayKind::Footway
1499 | WayKind::Sidewalk
1500 | WayKind::Crossing
1501 | WayKind::PedestrianStreet
1502 | WayKind::Steps
1503 | WayKind::Bridleway
1504 | WayKind::Bushwhack
1505 | WayKind::Cycleway => 0,
1506 WayKind::Track => 1,
1507 WayKind::ServiceRoad => 2,
1508 WayKind::Roadway | WayKind::Unknown => 3,
1509 };
1510 (access, geometry, function)
1511}
1512
1513fn route_span_fits_edge(
1514 edge: &Edge,
1515 line: &LineString,
1516 first_segment: usize,
1517 last_segment: usize,
1518 max_snap_m: f64,
1519) -> bool {
1520 line.points
1521 .windows(2)
1522 .enumerate()
1523 .skip(first_segment)
1524 .take(last_segment.saturating_sub(first_segment) + 1)
1525 .all(|(_, segment)| edge_distance_m(edge, segment[0].lerp(segment[1], 0.5)) <= max_snap_m)
1526}
1527
1528fn line_progress_m(line: &LineString, point: Coord) -> f64 {
1529 line_projection(line, point).map_or(0.0, |projection| projection.1)
1530}
1531
1532pub(crate) fn line_projection(line: &LineString, point: Coord) -> Option<(f64, f64, Coord)> {
1533 let mut traversed_m = 0.0;
1534 let mut nearest = None::<(f64, f64, Coord)>;
1535 for segment in line.points.windows(2) {
1536 let length_m = segment[0].haversine_m(segment[1]);
1537 let (distance_m, interpolation) = segment_projection(segment[0], segment[1], point);
1538 if nearest.is_none_or(|nearest| distance_m < nearest.0) {
1539 nearest = Some((
1540 distance_m,
1541 length_m.mul_add(interpolation, traversed_m),
1542 segment[0].lerp(segment[1], interpolation),
1543 ));
1544 }
1545 traversed_m += length_m;
1546 }
1547 nearest
1548}
1549
1550fn segment_projection(head: Coord, tail: Coord, point: Coord) -> (f64, f64) {
1551 let latitude_scale = point.lat.to_radians().cos();
1552 let meters_per_lon = 111_320.0 * latitude_scale;
1553 let meters_per_lat = 110_540.0;
1554 let head_x = (head.lon - point.lon) * meters_per_lon;
1555 let head_y = (head.lat - point.lat) * meters_per_lat;
1556 let tail_x = (tail.lon - point.lon) * meters_per_lon;
1557 let tail_y = (tail.lat - point.lat) * meters_per_lat;
1558 let span_x = tail_x - head_x;
1559 let span_y = tail_y - head_y;
1560 let span_len2 = span_x.mul_add(span_x, span_y * span_y);
1561 if span_len2 <= f64::EPSILON {
1562 return (head_x.hypot(head_y), 0.0);
1563 }
1564 let interpolation = (-(head_x * span_x + head_y * span_y) / span_len2).clamp(0.0, 1.0);
1565 (
1566 (head_x + span_x * interpolation).hypot(head_y + span_y * interpolation),
1567 interpolation,
1568 )
1569}