Skip to main content

trailgen_core/
seed.rs

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}