1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! Global route planner for high-level path planning using A* pathfinding.
use super::types::RoadOption;
use crate::{
client::{Map, Waypoint},
error::{MapError, Result},
geom::Location,
};
use petgraph::{
Directed,
graph::{Graph, NodeIndex},
};
use std::collections::HashMap;
/// Node in the road topology graph.
#[derive(Debug, Clone)]
struct RoadNode {
waypoint: Waypoint,
location: Location,
}
/// Edge in the road topology graph.
#[derive(Debug, Clone)]
struct RoadEdge {
/// Length of this road segment in meters
length: f32,
/// Road option for this edge (left turn, right turn, etc.)
road_option: RoadOption,
}
/// Global route planner that computes optimal routes using A* pathfinding.
///
/// Builds a graph from the CARLA map topology and uses A* to find
/// the shortest path between locations.
pub struct GlobalRoutePlanner {
map: Map,
#[allow(dead_code)]
sampling_resolution: f32,
/// Graph structure for pathfinding
graph: Graph<RoadNode, RoadEdge, Directed>,
/// Map from waypoint ID to graph node index
waypoint_to_node: HashMap<u64, NodeIndex>,
}
impl GlobalRoutePlanner {
/// Creates a new GlobalRoutePlanner and builds the road topology graph.
///
/// # Arguments
/// * `map` - CARLA map instance
/// * `sampling_resolution` - Distance between route waypoints in meters
pub fn new(map: Map, sampling_resolution: f32) -> Result<Self> {
let mut planner = Self {
map,
sampling_resolution,
graph: Graph::new(),
waypoint_to_node: HashMap::new(),
};
planner.build_graph()?;
Ok(planner)
}
/// Builds the road topology graph from the CARLA map.
fn build_graph(&mut self) -> Result<()> {
// Get topology from map
let topology = self.map.topology()?;
// Phase 1: Create nodes for all waypoints in topology
for (start_wp, end_wp) in topology.iter() {
// Add start waypoint if not already present
if !self.waypoint_to_node.contains_key(&start_wp.id()) {
let location = start_wp.transform().location;
let node = RoadNode {
waypoint: start_wp.clone(),
location,
};
let node_idx = self.graph.add_node(node);
self.waypoint_to_node.insert(start_wp.id(), node_idx);
}
// Add end waypoint if not already present
if !self.waypoint_to_node.contains_key(&end_wp.id()) {
let location = end_wp.transform().location;
let node = RoadNode {
waypoint: end_wp.clone(),
location,
};
let node_idx = self.graph.add_node(node);
self.waypoint_to_node.insert(end_wp.id(), node_idx);
}
}
// Phase 2: Create edges between connected waypoints
for (start_wp, end_wp) in topology.iter() {
// Skip if either node wasn't added (shouldn't happen with new topology filtering)
let start_idx = match self.waypoint_to_node.get(&start_wp.id()) {
Some(&idx) => idx,
None => continue,
};
let end_idx = match self.waypoint_to_node.get(&end_wp.id()) {
Some(&idx) => idx,
None => continue,
};
// Calculate edge length
let start_loc = start_wp.transform().location;
let end_loc = end_wp.transform().location;
let length = ((end_loc.x - start_loc.x).powi(2)
+ (end_loc.y - start_loc.y).powi(2)
+ (end_loc.z - start_loc.z).powi(2))
.sqrt();
// Determine road option based on waypoint relationship
let road_option = self.compute_road_option(start_wp, end_wp);
let edge = RoadEdge {
length,
road_option,
};
self.graph.add_edge(start_idx, end_idx, edge);
}
// Phase 3: Add lane change edges
self.add_lane_change_edges();
Ok(())
}
/// Adds lane change edges to the graph.
fn add_lane_change_edges(&mut self) {
// Collect all waypoints that have lane change options
let waypoints: Vec<_> = self.waypoint_to_node.keys().copied().collect();
// Collect edges to add (to avoid borrowing issues)
let mut edges_to_add: Vec<(NodeIndex, NodeIndex, RoadEdge)> = Vec::new();
for wp_id in waypoints {
let node_idx = self.waypoint_to_node[&wp_id];
let node = &self.graph[node_idx];
let waypoint = node.waypoint.clone();
// Check for left lane
if let Ok(Some(left_wp)) = waypoint.left()
&& let Some(&left_idx) = self.waypoint_to_node.get(&left_wp.id())
{
// Add lane change left edge
let edge = RoadEdge {
length: 1.0, // Small cost for lane changes
road_option: RoadOption::ChangeLaneLeft,
};
edges_to_add.push((node_idx, left_idx, edge));
}
// Check for right lane
if let Ok(Some(right_wp)) = waypoint.right()
&& let Some(&right_idx) = self.waypoint_to_node.get(&right_wp.id())
{
// Add lane change right edge
let edge = RoadEdge {
length: 1.0, // Small cost for lane changes
road_option: RoadOption::ChangeLaneRight,
};
edges_to_add.push((node_idx, right_idx, edge));
}
}
// Now add all the edges
for (from, to, edge) in edges_to_add {
self.graph.add_edge(from, to, edge);
}
}
/// Computes the RoadOption for an edge based on the turn angle.
fn compute_road_option(&self, start_wp: &Waypoint, end_wp: &Waypoint) -> RoadOption {
// Get transforms
let start_transform = start_wp.transform();
let end_transform = end_wp.transform();
// Get forward vectors
let start_forward = start_transform.rotation.forward_vector();
let end_forward = end_transform.rotation.forward_vector();
// Calculate the cross product to determine turn direction
let cross_z = start_forward.x * end_forward.y - start_forward.y * end_forward.x;
// Calculate the dot product to determine if it's a turn or straight
let dot = start_forward.x * end_forward.x + start_forward.y * end_forward.y;
// Thresholds for turn classification
const TURN_THRESHOLD: f32 = 0.1;
const STRAIGHT_THRESHOLD: f32 = 0.7;
if dot > STRAIGHT_THRESHOLD {
RoadOption::LaneFollow
} else if cross_z > TURN_THRESHOLD {
RoadOption::Left
} else if cross_z < -TURN_THRESHOLD {
RoadOption::Right
} else {
RoadOption::Straight
}
}
/// Computes an optimal route from origin to destination using A* pathfinding.
///
/// # Arguments
/// * `origin` - Starting location
/// * `destination` - Target location
///
/// # Returns
/// Vector of (Waypoint, RoadOption) pairs representing the optimal route
pub fn trace_route(
&self,
origin: Location,
destination: Location,
) -> Result<Vec<(Waypoint, RoadOption)>> {
// Get starting and ending waypoints
let start_wp = self
.map
.waypoint_at(&origin)?
.ok_or_else(|| MapError::InvalidWaypoint {
location: format!("{:?}", origin),
reason: "Could not find waypoint at origin".to_string(),
})?;
let end_wp =
self.map
.waypoint_at(&destination)?
.ok_or_else(|| MapError::InvalidWaypoint {
location: format!("{:?}", destination),
reason: "Could not find waypoint at destination".to_string(),
})?;
// Find closest nodes in graph to start and end waypoints
let start_node = self.find_closest_node(&start_wp)?;
let end_node = self.find_closest_node(&end_wp)?;
// Run A* pathfinding
let path = petgraph::algo::astar(
&self.graph,
start_node,
|node| node == end_node,
|edge| edge.weight().length,
|node| {
// Heuristic: Euclidean distance to goal
let node_loc = &self.graph[node].location;
let end_loc = &self.graph[end_node].location;
((node_loc.x - end_loc.x).powi(2)
+ (node_loc.y - end_loc.y).powi(2)
+ (node_loc.z - end_loc.z).powi(2))
.sqrt()
},
);
let (_cost, node_path) = path.ok_or(MapError::TopologyError {
message: "No path found between origin and destination".to_string(),
})?;
// Convert node path to waypoint route with road options
let mut route = Vec::new();
for i in 0..node_path.len() {
let node_idx = node_path[i];
let waypoint = self.graph[node_idx].waypoint.clone();
// Determine road option from edge to next node
let road_option = if i + 1 < node_path.len() {
let next_node_idx = node_path[i + 1];
// Find edge between current and next node
self.graph
.edges_connecting(node_idx, next_node_idx)
.next()
.map(|edge| edge.weight().road_option)
.unwrap_or(RoadOption::LaneFollow)
} else {
RoadOption::LaneFollow
};
route.push((waypoint, road_option));
}
if route.is_empty() {
return Err(MapError::TopologyError {
message: "Failed to compute route".to_string(),
}
.into());
}
Ok(route)
}
/// Finds the closest node in the graph to the given waypoint.
fn find_closest_node(&self, waypoint: &Waypoint) -> Result<NodeIndex> {
let wp_loc = waypoint.transform().location;
// Check if waypoint is directly in the graph
if let Some(&node_idx) = self.waypoint_to_node.get(&waypoint.id()) {
return Ok(node_idx);
}
// Find closest node by distance
let mut closest_node: Option<NodeIndex> = None;
let mut min_distance = f32::INFINITY;
for node_idx in self.graph.node_indices() {
let node_loc = &self.graph[node_idx].location;
let distance = ((wp_loc.x - node_loc.x).powi(2)
+ (wp_loc.y - node_loc.y).powi(2)
+ (wp_loc.z - node_loc.z).powi(2))
.sqrt();
if distance < min_distance {
min_distance = distance;
closest_node = Some(node_idx);
}
}
closest_node.ok_or_else(|| {
MapError::TopologyError {
message: "No nodes in graph".to_string(),
}
.into()
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_global_route_planner_creation() {
// This test just checks that the struct can be created
// Full testing requires a CARLA connection
let sampling_resolution = 2.0;
assert_eq!(sampling_resolution, 2.0);
}
}