1use crate::constraints::LoopConstraints;
2use crate::geo::LineString;
3use crate::io::route_file::RouteFileMetadata;
4use crate::model::{EdgeId, Provenance, RouteSnapStats, VertexId, WalkGraph};
5use crate::route::{Route, RouteMetrics};
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub struct SeedRoute {
10 pub name: String,
11 pub source_path: String,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub original_source_path: Option<String>,
14 pub source_format: String,
15 #[serde(default, skip_serializing_if = "RouteFileMetadata::is_empty")]
16 pub metadata: RouteFileMetadata,
17 pub point_count: usize,
18 pub snapped_edges: Vec<EdgeId>,
19 #[serde(default)]
20 pub snap: RouteSnapStats,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub start: Option<VertexId>,
23 pub closed_loop: bool,
24 pub metrics: RouteMetrics,
25 pub provenance: Provenance,
26}
27
28impl SeedRoute {
29 #[must_use]
30 pub fn snap(
31 graph: &WalkGraph,
32 name: impl Into<String>,
33 source_path: impl Into<String>,
34 source_format: impl Into<String>,
35 line: &LineString,
36 ) -> Self {
37 Self::snap_with_limit(graph, name, source_path, source_format, line, f64::INFINITY)
38 }
39
40 #[must_use]
41 pub fn snap_with_limit(
42 graph: &WalkGraph,
43 name: impl Into<String>,
44 source_path: impl Into<String>,
45 source_format: impl Into<String>,
46 line: &LineString,
47 max_snap_m: f64,
48 ) -> Self {
49 let snap = graph.trace_coverage(line, max_snap_m);
50 let snapped_edges = snap.edges;
51 let candidate_start = graph.snapped_line_start(line, &snapped_edges);
52 let finish =
53 candidate_start.and_then(|v| graph.walk_edges(v, &snapped_edges).map(|f| (v, f)));
54 let start = finish.map(|(start, _)| start);
55 let closed_loop = finish.is_some_and(|(start, finish)| finish == start);
56 let metrics = start.map_or_else(RouteMetrics::default, |v| {
57 RouteMetrics::measure(graph, v, &snapped_edges)
58 });
59 let source_path = source_path.into();
60 let source_format = source_format.into();
61 let name = name.into();
62 let provenance = Provenance {
63 source: "seed-route".to_owned(),
64 layer: Some(source_format.clone()),
65 source_id: Some(name.clone()),
66 license: None,
67 };
68 Self {
69 name,
70 source_path,
71 original_source_path: None,
72 source_format,
73 metadata: RouteFileMetadata::default(),
74 point_count: line.points.len(),
75 snapped_edges,
76 snap: snap.stats,
77 start,
78 closed_loop,
79 metrics,
80 provenance,
81 }
82 }
83
84 #[must_use]
85 pub fn as_route(&self, graph: &WalkGraph, constraints: &LoopConstraints) -> Option<Route> {
86 let start = self.start?;
87 if !self.closed_loop {
88 return None;
89 }
90 Some(Route::from_edges(
91 format!("seed-{}", artifact_key(&self.name)),
92 graph,
93 start,
94 self.snapped_edges.clone(),
95 constraints,
96 ))
97 }
98}
99
100#[must_use]
101pub fn artifact_key(raw: &str) -> String {
102 let mut stem = String::with_capacity(raw.len().min(64));
103 for c in raw.chars() {
104 if stem.len() == 64 {
105 break;
106 }
107 if c.is_ascii_alphanumeric() {
108 stem.push(c.to_ascii_lowercase());
109 } else if !stem.is_empty() && !stem.ends_with('-') {
110 stem.push('-');
111 }
112 }
113 while stem.ends_with('-') {
114 stem.pop();
115 }
116 if stem.is_empty() {
117 stem.push_str("route");
118 }
119 let scar = raw
120 .as_bytes()
121 .iter()
122 .fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
123 (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
124 });
125 format!("{stem}-{scar:016x}")
126}