1use std::collections::HashSet;
8
9use crate::grid::{GridMap, Node};
10use rust_robotics_core::{Obstacles, Path2D, PathPlanner, Point2D, RoboticsError, RoboticsResult};
11
12#[derive(Debug, Clone)]
14pub struct DFSConfig {
15 pub resolution: f64,
17 pub robot_radius: f64,
19}
20
21impl Default for DFSConfig {
22 fn default() -> Self {
23 Self {
24 resolution: 1.0,
25 robot_radius: 0.5,
26 }
27 }
28}
29
30impl DFSConfig {
31 pub fn validate(&self) -> RoboticsResult<()> {
32 if !self.resolution.is_finite() || self.resolution <= 0.0 {
33 return Err(RoboticsError::InvalidParameter(format!(
34 "resolution must be positive and finite, got {}",
35 self.resolution
36 )));
37 }
38 if !self.robot_radius.is_finite() || self.robot_radius < 0.0 {
39 return Err(RoboticsError::InvalidParameter(format!(
40 "robot_radius must be non-negative and finite, got {}",
41 self.robot_radius
42 )));
43 }
44
45 Ok(())
46 }
47}
48
49pub struct DFSPlanner {
51 grid_map: GridMap,
52 #[allow(dead_code)]
53 config: DFSConfig,
54 motion: Vec<(i32, i32, f64)>,
55}
56
57impl DFSPlanner {
58 pub fn new(ox: &[f64], oy: &[f64], config: DFSConfig) -> Self {
60 Self::try_new(ox, oy, config).expect(
61 "invalid DFS planner input: obstacle list must be non-empty and valid, and config values must be positive/finite",
62 )
63 }
64
65 pub fn try_new(ox: &[f64], oy: &[f64], config: DFSConfig) -> RoboticsResult<Self> {
67 config.validate()?;
68 let grid_map = GridMap::try_new(ox, oy, config.resolution, config.robot_radius)?;
69 let motion = Self::get_motion_model();
70
71 Ok(DFSPlanner {
72 grid_map,
73 config,
74 motion,
75 })
76 }
77
78 pub fn from_obstacles(ox: &[f64], oy: &[f64], resolution: f64, robot_radius: f64) -> Self {
80 let config = DFSConfig {
81 resolution,
82 robot_radius,
83 };
84 Self::new(ox, oy, config)
85 }
86
87 pub fn from_obstacle_points(obstacles: &Obstacles, config: DFSConfig) -> RoboticsResult<Self> {
89 config.validate()?;
90 let grid_map = GridMap::from_obstacles(obstacles, config.resolution, config.robot_radius)?;
91 let motion = Self::get_motion_model();
92
93 Ok(DFSPlanner {
94 grid_map,
95 config,
96 motion,
97 })
98 }
99
100 #[deprecated(note = "use plan() or plan_xy() instead")]
102 pub fn planning(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> Option<(Vec<f64>, Vec<f64>)> {
103 match self.plan_xy(sx, sy, gx, gy) {
104 Ok(path) => Some((path.x_coords(), path.y_coords())),
105 Err(_) => None,
106 }
107 }
108
109 pub fn plan(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
111 self.plan_impl(start, goal)
112 }
113
114 pub fn plan_xy(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> RoboticsResult<Path2D> {
116 self.plan_impl(Point2D::new(sx, sy), Point2D::new(gx, gy))
117 }
118
119 pub fn grid_map(&self) -> &GridMap {
121 &self.grid_map
122 }
123
124 fn get_motion_model() -> Vec<(i32, i32, f64)> {
125 vec![
127 (1, 0, 1.0),
128 (0, 1, 1.0),
129 (-1, 0, 1.0),
130 (0, -1, 1.0),
131 (-1, -1, std::f64::consts::SQRT_2),
132 (-1, 1, std::f64::consts::SQRT_2),
133 (1, -1, std::f64::consts::SQRT_2),
134 (1, 1, std::f64::consts::SQRT_2),
135 ]
136 }
137
138 fn build_path(&self, goal_index: usize, node_storage: &[Node]) -> Path2D {
139 let mut points = Vec::new();
140 let mut current_index = Some(goal_index);
141
142 while let Some(index) = current_index {
143 let node = &node_storage[index];
144 points.push(Point2D::new(
145 self.grid_map.calc_x_position(node.x),
146 self.grid_map.calc_y_position(node.y),
147 ));
148 current_index = node.parent_index;
149 }
150
151 points.reverse();
152 Path2D::from_points(points)
153 }
154
155 fn ensure_query_is_valid(&self, x: i32, y: i32, label: &str) -> RoboticsResult<()> {
156 if self.grid_map.is_valid(x, y) {
157 return Ok(());
158 }
159
160 Err(RoboticsError::PlanningError(format!(
161 "{} position is invalid",
162 label
163 )))
164 }
165
166 fn plan_impl(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
167 let start_x = self.grid_map.calc_x_index(start.x);
168 let start_y = self.grid_map.calc_y_index(start.y);
169 let goal_x = self.grid_map.calc_x_index(goal.x);
170 let goal_y = self.grid_map.calc_y_index(goal.y);
171
172 self.ensure_query_is_valid(start_x, start_y, "Start")?;
173 self.ensure_query_is_valid(goal_x, goal_y, "Goal")?;
174
175 let mut stack: Vec<usize> = Vec::new();
177 let mut closed_set: HashSet<i32> = HashSet::new();
178 let mut node_storage: Vec<Node> = Vec::new();
179
180 node_storage.push(Node::new(start_x, start_y, 0.0, None));
182 let start_storage_index = 0;
183 stack.push(start_storage_index);
184
185 while let Some(current_storage_index) = stack.pop() {
186 let current_x = node_storage[current_storage_index].x;
187 let current_y = node_storage[current_storage_index].y;
188 let current_cost = node_storage[current_storage_index].cost;
189 let current_grid_index = self.grid_map.calc_index(current_x, current_y);
190
191 if current_x == goal_x && current_y == goal_y {
193 return Ok(self.build_path(current_storage_index, &node_storage));
194 }
195
196 if closed_set.contains(¤t_grid_index) {
198 continue;
199 }
200
201 closed_set.insert(current_grid_index);
203
204 for &(dx, dy, move_cost) in &self.motion {
206 let new_x = current_x + dx;
207 let new_y = current_y + dy;
208 let new_cost = current_cost + move_cost;
209 let new_grid_index = self.grid_map.calc_index(new_x, new_y);
210
211 if !self.grid_map.is_valid_offset(current_x, current_y, dx, dy) {
213 continue;
214 }
215 if closed_set.contains(&new_grid_index) {
216 continue;
217 }
218
219 node_storage.push(Node::new(
221 new_x,
222 new_y,
223 new_cost,
224 Some(current_storage_index),
225 ));
226 let new_storage_index = node_storage.len() - 1;
227 stack.push(new_storage_index);
228 }
229 }
230
231 Err(RoboticsError::PlanningError("No path found".to_string()))
232 }
233}
234
235impl PathPlanner for DFSPlanner {
236 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
237 self.plan_impl(start, goal)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 use rust_robotics_core::Obstacles;
246
247 fn create_simple_obstacles() -> (Vec<f64>, Vec<f64>) {
248 let mut ox = Vec::new();
249 let mut oy = Vec::new();
250
251 for i in 0..11 {
253 ox.push(i as f64);
254 oy.push(0.0);
255 ox.push(i as f64);
256 oy.push(10.0);
257 ox.push(0.0);
258 oy.push(i as f64);
259 ox.push(10.0);
260 oy.push(i as f64);
261 }
262
263 for i in 4..7 {
265 ox.push(5.0);
266 oy.push(i as f64);
267 }
268
269 (ox, oy)
270 }
271
272 #[test]
273 fn test_dfs_finds_path() {
274 let (ox, oy) = create_simple_obstacles();
275 let planner = DFSPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
276
277 let start = Point2D::new(2.0, 2.0);
278 let goal = Point2D::new(8.0, 8.0);
279
280 let result = planner.plan(start, goal);
281 assert!(result.is_ok());
282
283 let path = result.unwrap();
284 assert!(!path.is_empty());
285 }
286
287 #[test]
288 #[allow(deprecated)]
289 fn test_dfs_legacy_interface() {
290 let (ox, oy) = create_simple_obstacles();
291 let planner = DFSPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
292
293 let result = planner.planning(2.0, 2.0, 8.0, 8.0);
294 assert!(result.is_some());
295
296 let (rx, ry) = result.unwrap();
297 assert!(!rx.is_empty());
298 assert_eq!(rx.len(), ry.len());
299 }
300
301 #[test]
302 fn test_dfs_from_obstacle_points() {
303 let (ox, oy) = create_simple_obstacles();
304 let obstacles = Obstacles::try_from_xy(&ox, &oy).unwrap();
305 let planner = DFSPlanner::from_obstacle_points(&obstacles, DFSConfig::default()).unwrap();
306
307 let path = planner.plan_xy(2.0, 2.0, 8.0, 8.0).unwrap();
308 assert!(!path.is_empty());
309 }
310
311 #[test]
312 fn test_dfs_try_new_rejects_invalid_config() {
313 let (ox, oy) = create_simple_obstacles();
314 let config = DFSConfig {
315 resolution: 0.0,
316 ..Default::default()
317 };
318
319 let err = match DFSPlanner::try_new(&ox, &oy, config) {
320 Ok(_) => panic!("expected invalid config to be rejected"),
321 Err(err) => err,
322 };
323 assert!(matches!(err, RoboticsError::InvalidParameter(_)));
324 }
325
326 #[test]
327 fn test_dfs_preserves_asymmetric_world_coordinates() {
328 let mut obstacles = Obstacles::new();
329
330 for x in 10..=20 {
331 obstacles.push(Point2D::new(x as f64, -4.0));
332 obstacles.push(Point2D::new(x as f64, 6.0));
333 }
334 for y in -4..=6 {
335 obstacles.push(Point2D::new(10.0, y as f64));
336 obstacles.push(Point2D::new(20.0, y as f64));
337 }
338
339 let planner = DFSPlanner::from_obstacle_points(&obstacles, DFSConfig::default()).unwrap();
340 let path = planner.plan_xy(12.0, -2.0, 18.0, 4.0).unwrap();
341
342 let first = path.points.first().unwrap();
343 let last = path.points.last().unwrap();
344 assert!((first.x - 12.0).abs() < 1e-6);
345 assert!((first.y + 2.0).abs() < 1e-6);
346 assert!((last.x - 18.0).abs() < 1e-6);
347 assert!((last.y - 4.0).abs() < 1e-6);
348 }
349
350 #[test]
351 fn test_dfs_path_starts_at_start_and_ends_at_goal() {
352 let (ox, oy) = create_simple_obstacles();
353 let planner = DFSPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
354
355 let path = planner.plan_xy(2.0, 2.0, 8.0, 8.0).unwrap();
356 let first = path.points.first().unwrap();
357 let last = path.points.last().unwrap();
358
359 assert!((first.x - 2.0).abs() < 1e-6);
360 assert!((first.y - 2.0).abs() < 1e-6);
361 assert!((last.x - 8.0).abs() < 1e-6);
362 assert!((last.y - 8.0).abs() < 1e-6);
363 }
364}