routee_compass_core/model/map/geometry_model.rs
1use std::sync::Arc;
2
3use super::map_error::MapError;
4use crate::{
5 model::network::{EdgeId, EdgeListId, Graph},
6 util::{fs::read_utils, geo::geo_io_utils},
7};
8use geo::LineString;
9use kdam::{Bar, BarExt};
10
11/// model for link geometries by edge id. can be constructed either
12/// from edge geometry dataset ([`GeometryModel::new_from_edges`]) or
13/// from the vertices ([`GeometryModel::new_from_vertices`]) by simply
14/// drawing lines between coordinates.
15pub struct GeometryModel(Vec<LineString<f32>>);
16
17impl GeometryModel {
18 /// with no provided geometries, create minimal LineStrings from pairs of vertex Points
19 pub fn new_from_vertices(
20 graph: Arc<Graph>,
21 edge_list_id: EdgeListId,
22 ) -> Result<GeometryModel, MapError> {
23 let edges = create_linestrings_from_vertices(graph, edge_list_id)?;
24 Ok(GeometryModel(edges))
25 }
26
27 /// use a user-provided enumerated textfile input to load LineString geometries
28 pub fn new_from_edges(
29 geometry_input_file: &String,
30 edge_list_id: EdgeListId,
31 graph: Arc<Graph>,
32 ) -> Result<GeometryModel, MapError> {
33 let edge_list = graph.get_edge_list(&edge_list_id).map_err(|e| {
34 MapError::BuildError(format!(
35 "while creating GeometryModel for input file {geometry_input_file}, {e}"
36 ))
37 })?;
38
39 let edge_list_len = edge_list.len();
40 let linestrings = read_linestrings(geometry_input_file, edge_list_len)?;
41
42 if linestrings.len() != edge_list_len {
43 Err(MapError::BuildError(format!("edge list {edge_list_id} geometry file {geometry_input_file} should have {edge_list_len} rows, found {}", linestrings.len())))
44 } else {
45 Ok(GeometryModel(linestrings))
46 }
47 }
48
49 /// iterate through the geometries of this model
50 pub fn geometries<'a>(&'a self) -> Box<dyn Iterator<Item = &'a LineString<f32>> + 'a> {
51 Box::new(self.0.iter())
52 }
53
54 /// get a single geometry by it's EdgeId
55 pub fn get<'a>(&'a self, edge_id: &EdgeId) -> Option<&'a LineString<f32>> {
56 self.0.get(edge_id.0)
57 }
58}
59
60fn read_linestrings(
61 geometry_input_file: &String,
62 n_edges: usize,
63) -> Result<Vec<geo::LineString<f32>>, MapError> {
64 let geoms = read_utils::read_raw_file(
65 geometry_input_file,
66 geo_io_utils::parse_wkt_linestring,
67 Some(Bar::builder().desc("link geometries").total(n_edges)),
68 None,
69 )
70 .map_err(|e: std::io::Error| {
71 MapError::BuildError(format!("error loading {geometry_input_file}: {e}"))
72 })?
73 .to_vec();
74 eprintln!();
75 Ok(geoms)
76}
77
78fn create_linestrings_from_vertices(
79 graph: Arc<Graph>,
80 edge_list_id: EdgeListId,
81) -> Result<Vec<LineString<f32>>, MapError> {
82 let edge_list = graph.get_edge_list(&edge_list_id).map_err(|e| {
83 MapError::BuildError(format!("while creating GeometryModel from vertices, {e}"))
84 })?;
85
86 let n_edges = edge_list.len();
87 let mut pb = kdam::Bar::builder()
88 .total(n_edges)
89 .animation("fillup")
90 .desc("edge LineString geometry file")
91 .build()
92 .map_err(MapError::InternalError)?;
93
94 let edges = edge_list
95 .edges()
96 .map(|e| {
97 let src_v = graph.get_vertex(&e.src_vertex_id).map_err(|_| {
98 MapError::InternalError(format!(
99 "edge {} src vertex {} missing",
100 e.edge_id, e.src_vertex_id
101 ))
102 })?;
103 let dst_v = graph.get_vertex(&e.dst_vertex_id).map_err(|_| {
104 MapError::InternalError(format!(
105 "edge {} dst vertex {} missing",
106 e.edge_id, e.dst_vertex_id
107 ))
108 })?;
109
110 let linestring = geo::line_string![src_v.coordinate.0, dst_v.coordinate.0,];
111 let _ = pb.update(1);
112 Ok(linestring)
113 })
114 .collect::<Result<Vec<_>, MapError>>()?;
115
116 eprintln!();
117 Ok(edges)
118}
119
120// TODO:
121// - the API for OutputPlugin now expects a SearchInstance which is non-trivial to instantiate.
122// the logic for adding geometries should be refactored into a separate function and this test
123// should be moved to the file where that function exists.
124// - the loading of geometries is now handled by the MapModel. testing geometry retrieval and
125// linestring reconstruction should be moved to the map_model.rs file.
126
127#[cfg(test)]
128mod tests {
129
130 use crate::util::{fs::read_utils::read_raw_file, geo::geo_io_utils::parse_wkt_linestring};
131
132 use std::path::PathBuf;
133
134 fn mock_geometry_file() -> PathBuf {
135 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
136 .join("src")
137 .join("model")
138 .join("map")
139 .join("test")
140 .join("geometries.txt")
141 }
142
143 // fn mock_graph() -> Graph {
144 // let edges = Box::new([
145 // Edge::new(0, 0, 2, 2.8284271247),
146 // Edge::new(1, 3, 5, 2.8284271247),
147 // Edge::new(2, 6, 8, 2.8284271247),
148 // ]);
149 // let vertices = Box::new([
150 // Vertex::new(0, 0.0, 0.0),
151 // Vertex::new(1, 1.0, 1.0),
152 // Vertex::new(2, 2.0, 2.0),
153 // Vertex::new(3, 3.0, 3.0),
154 // Vertex::new(4, 4.0, 4.0),
155 // Vertex::new(5, 5.0, 5.0),
156 // Vertex::new(6, 6.0, 6.0),
157 // Vertex::new(7, 7.0, 7.0),
158 // Vertex::new(8, 8.0, 8.0),
159 // ]);
160 // let adj = Box::new([
161 // CompactOrderedHashMap::new(vec![(EdgeId(0), VertexId(2))]),
162 // CompactOrderedHashMap::empty(),
163 // CompactOrderedHashMap::empty(),
164 // CompactOrderedHashMap::new(vec![(EdgeId(1), VertexId(5))]),
165 // CompactOrderedHashMap::empty(),
166 // CompactOrderedHashMap::empty(),
167 // CompactOrderedHashMap::new(vec![(EdgeId(2), VertexId(8))]),
168 // CompactOrderedHashMap::empty(),
169 // CompactOrderedHashMap::empty(),
170 // ]);
171 // let rev = Box::new([
172 // CompactOrderedHashMap::empty(),
173 // CompactOrderedHashMap::empty(),
174 // CompactOrderedHashMap::new(vec![(EdgeId(0), VertexId(0))]),
175 // CompactOrderedHashMap::empty(),
176 // CompactOrderedHashMap::empty(),
177 // CompactOrderedHashMap::new(vec![(EdgeId(1), VertexId(3))]),
178 // CompactOrderedHashMap::empty(),
179 // CompactOrderedHashMap::empty(),
180 // CompactOrderedHashMap::new(vec![(EdgeId(2), VertexId(6))]),
181 // ]);
182 // Graph {
183 // adj,
184 // rev,
185 // edges,
186 // vertices,
187 // }
188 // }
189
190 #[test]
191 fn test_geometry_deserialization() {
192 let result = read_raw_file(mock_geometry_file(), parse_wkt_linestring, None, None).unwrap();
193 assert_eq!(result.len(), 3);
194 }
195
196 // #[ignore = "no ideal candidate module for this unit test. TraversalOutputFormat concatenates linestrings but is too high-level for this test"]
197 // fn test_add_geometry() {
198 // let geoms_filepath = mock_geometry_file();
199 // let geoms_file_string = geoms_filepath.to_str().unwrap().to_string();
200 // let graph = Arc::new(mock_graph());
201 // let geometry_model =
202 // GeometryModel::new_from_edges(&geoms_file_string, graph.clone()).unwrap();
203
204 // OLD TEST STUB:
205 // let expected_geometry = String::from("LINESTRING(0 0,1 1,2 2,3 3,4 4,5 5,6 6,7 7,8 8)");
206 // let mut output_result = serde_json::json!({});
207 // let route = vec![
208 // EdgeTraversal {
209 // edge_id: EdgeId(0),
210 // access_cost: Cost::from(0.0),
211 // traversal_cost: Cost::from(0.0),
212 // result_state: vec![StateVar(0.0)],
213 // },
214 // EdgeTraversal {
215 // edge_id: EdgeId(1),
216 // access_cost: Cost::from(0.0),
217 // traversal_cost: Cost::from(0.0),
218 // result_state: vec![StateVar(0.0)],
219 // },
220 // EdgeTraversal {
221 // edge_id: EdgeId(2),
222 // access_cost: Cost::from(0.0),
223 // traversal_cost: Cost::from(0.0),
224 // result_state: vec![StateVar(0.0)],
225 // },
226 // ];
227 // let search_result = SearchAppResult {
228 // route,
229 // tree: HashMap::new(),
230 // search_executed_time: Local::now().to_rfc3339(),
231 // algorithm_runtime: Duration::ZERO,
232 // route_runtime: Duration::ZERO,
233 // search_app_runtime: Duration::ZERO,
234 // iterations: 0,
235 // };
236 // let filename = mock_geometry_file();
237 // let _route_geometry = true;
238 // let _tree_geometry = false;
239 // let geom_plugin =
240 // TraversalPlugin::from_file(&filename, Some(TraversalOutputFormat::Wkt), None).unwrap();
241
242 // geom_plugin
243 // .process(&mut output_result, &Ok(search_result))
244 // .unwrap();
245 // let geometry_wkt = output_result.get_route_geometry_wkt().unwrap();
246 // assert_eq!(geometry_wkt, expected_geometry);
247 // }
248}