Skip to main content

rustsim_pathfinding/
route.rs

1//! Route planning and step-along-route API for agents.
2//!
3//! Mirrors Julia Agents.jl `plan_route!`, `plan_best_route!`, and
4//! `move_along_route!` functions. Agents store their planned routes in
5//! a [`RoutePlanner`], then step along them one waypoint at a time.
6//!
7//! # Usage Pattern
8//!
9//! ```ignore
10//! use rustsim_pathfinding::route::RoutePlanner;
11//!
12//! let mut planner = RoutePlanner::new();
13//!
14//! // Plan a route for agent 1
15//! let waypoints = vec![(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)];
16//! planner.set_route(1, waypoints);
17//!
18//! // Move agent one step along the route at speed 1.5
19//! let result = planner.move_along_route_2d(
20//!     1,
21//!     (0.5, 1.0),  // current position
22//!     1.5,          // speed
23//!     1.0,          // dt
24//!     false,        // periodic
25//!     100.0,        // extent_x (only used when periodic)
26//!     100.0,        // extent_y
27//! );
28//! ```
29
30use std::collections::{HashMap, VecDeque};
31
32/// Per-agent route storage and movement controller.
33///
34/// Stores planned routes for agents (keyed by `AgentId = u64`) and
35/// provides `move_along_route` functions that advance agents along
36/// their planned paths at a given speed.
37///
38/// The generic parameter `W` is the waypoint type (e.g. `(f64, f64)`
39/// or `(f64, f64, f64)`).
40#[derive(Debug, Clone)]
41pub struct RoutePlanner<W> {
42    routes: HashMap<u64, VecDeque<W>>,
43}
44
45impl<W> Default for RoutePlanner<W> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl<W> RoutePlanner<W> {
52    /// Create a new empty route planner.
53    pub fn new() -> Self {
54        Self {
55            routes: HashMap::new(),
56        }
57    }
58
59    /// Set a planned route for an agent, replacing any existing route.
60    pub fn set_route(&mut self, agent_id: u64, waypoints: Vec<W>) {
61        self.routes.insert(agent_id, VecDeque::from(waypoints));
62    }
63
64    /// Check whether an agent has a non-empty route.
65    pub fn has_route(&self, agent_id: u64) -> bool {
66        self.routes
67            .get(&agent_id)
68            .map(|r| !r.is_empty())
69            .unwrap_or(false)
70    }
71
72    /// Check whether an agent is stationary (has no route or route is empty).
73    pub fn is_stationary(&self, agent_id: u64) -> bool {
74        !self.has_route(agent_id)
75    }
76
77    /// Get the remaining waypoints for an agent (read-only).
78    pub fn route(&self, agent_id: u64) -> Option<&VecDeque<W>> {
79        self.routes.get(&agent_id)
80    }
81
82    /// Remove all route data for an agent.
83    pub fn clear_route(&mut self, agent_id: u64) {
84        self.routes.remove(&agent_id);
85    }
86
87    /// Remove all route data for all agents.
88    pub fn clear_all(&mut self) {
89        self.routes.clear();
90    }
91
92    /// Number of agents with stored routes.
93    pub fn len(&self) -> usize {
94        self.routes.len()
95    }
96
97    /// Whether no agents have stored routes.
98    pub fn is_empty(&self) -> bool {
99        self.routes.is_empty()
100    }
101}
102
103/// 2D route planning result.
104#[derive(Debug, Clone, Copy)]
105pub struct MoveResult2D {
106    /// New position after movement.
107    pub position: (f64, f64),
108    /// Whether the agent reached the end of its route.
109    pub finished: bool,
110}
111
112impl RoutePlanner<(f64, f64)> {
113    /// Move an agent one step along its 2D route at the given `speed` and timestep `dt`.
114    ///
115    /// The agent moves from `current_pos` toward the next waypoint. If it
116    /// overshoots a waypoint, it continues toward the next one with the
117    /// remaining distance budget (mirrors Agents.jl `move_along_route!`).
118    ///
119    /// Returns the new position and whether the route is now complete.
120    /// If the agent has no route, returns `current_pos` unchanged.
121    ///
122    /// When `periodic` is true, movement wraps around `[0, extent_x) x [0, extent_y)`.
123    #[allow(clippy::too_many_arguments)]
124    pub fn move_along_route_2d(
125        &mut self,
126        agent_id: u64,
127        current_pos: (f64, f64),
128        speed: f64,
129        mut dt: f64,
130        periodic: bool,
131        extent_x: f64,
132        extent_y: f64,
133    ) -> MoveResult2D {
134        let route = match self.routes.get_mut(&agent_id) {
135            Some(r) if !r.is_empty() => r,
136            _ => {
137                return MoveResult2D {
138                    position: current_pos,
139                    finished: true,
140                };
141            }
142        };
143
144        let mut from = current_pos;
145        let mut next_pos = current_pos;
146
147        while let Some(&waypoint) = route.front() {
148            let dir = direction_2d(from, waypoint, periodic, extent_x, extent_y);
149            let dist_to_wp = (dir.0 * dir.0 + dir.1 * dir.1).sqrt();
150
151            if dist_to_wp < 1e-12 {
152                // Already at this waypoint
153                from = waypoint;
154                route.pop_front();
155                if route.is_empty() {
156                    next_pos = waypoint;
157                    break;
158                }
159                continue;
160            }
161
162            let move_dist = speed * dt;
163            let unit_dir = (dir.0 / dist_to_wp, dir.1 / dist_to_wp);
164
165            if move_dist >= dist_to_wp {
166                // Overshoot: consume this waypoint and continue
167                dt -= dist_to_wp / speed;
168                from = waypoint;
169                route.pop_front();
170                if route.is_empty() {
171                    next_pos = waypoint;
172                    break;
173                }
174            } else {
175                // Normal move
176                next_pos = (
177                    from.0 + unit_dir.0 * move_dist,
178                    from.1 + unit_dir.1 * move_dist,
179                );
180                if periodic {
181                    next_pos = normalize_pos_2d(next_pos, extent_x, extent_y);
182                }
183                break;
184            }
185        }
186
187        let finished = route.is_empty();
188        if finished {
189            self.routes.remove(&agent_id);
190        }
191
192        MoveResult2D {
193            position: next_pos,
194            finished,
195        }
196    }
197
198    /// Plan a route from a continuous A* path and store it.
199    ///
200    /// Convenience method: accepts the waypoints from
201    /// [`ContinuousPath`](crate::continuous_astar::ContinuousPath).
202    pub fn plan_from_path(&mut self, agent_id: u64, waypoints: Vec<(f64, f64)>) {
203        self.set_route(agent_id, waypoints);
204    }
205}
206
207/// 3D route planning result.
208#[derive(Debug, Clone, Copy)]
209pub struct MoveResult3D {
210    /// New position after movement.
211    pub position: (f64, f64, f64),
212    /// Whether the agent reached the end of its route.
213    pub finished: bool,
214}
215
216impl RoutePlanner<(f64, f64, f64)> {
217    /// Move an agent one step along its 3D route at the given `speed` and timestep `dt`.
218    ///
219    /// Same semantics as [`move_along_route_2d`](RoutePlanner::move_along_route_2d)
220    /// but in 3D.
221    #[allow(clippy::too_many_arguments)]
222    pub fn move_along_route_3d(
223        &mut self,
224        agent_id: u64,
225        current_pos: (f64, f64, f64),
226        speed: f64,
227        mut dt: f64,
228        periodic: bool,
229        extent_x: f64,
230        extent_y: f64,
231        extent_z: f64,
232    ) -> MoveResult3D {
233        let route = match self.routes.get_mut(&agent_id) {
234            Some(r) if !r.is_empty() => r,
235            _ => {
236                return MoveResult3D {
237                    position: current_pos,
238                    finished: true,
239                };
240            }
241        };
242
243        let mut from = current_pos;
244        let mut next_pos = current_pos;
245
246        while let Some(&waypoint) = route.front() {
247            let dir = direction_3d(from, waypoint, periodic, extent_x, extent_y, extent_z);
248            let dist_to_wp = (dir.0 * dir.0 + dir.1 * dir.1 + dir.2 * dir.2).sqrt();
249
250            if dist_to_wp < 1e-12 {
251                from = waypoint;
252                route.pop_front();
253                if route.is_empty() {
254                    next_pos = waypoint;
255                    break;
256                }
257                continue;
258            }
259
260            let move_dist = speed * dt;
261            let unit_dir = (dir.0 / dist_to_wp, dir.1 / dist_to_wp, dir.2 / dist_to_wp);
262
263            if move_dist >= dist_to_wp {
264                dt -= dist_to_wp / speed;
265                from = waypoint;
266                route.pop_front();
267                if route.is_empty() {
268                    next_pos = waypoint;
269                    break;
270                }
271            } else {
272                next_pos = (
273                    from.0 + unit_dir.0 * move_dist,
274                    from.1 + unit_dir.1 * move_dist,
275                    from.2 + unit_dir.2 * move_dist,
276                );
277                if periodic {
278                    next_pos = normalize_pos_3d(next_pos, extent_x, extent_y, extent_z);
279                }
280                break;
281            }
282        }
283
284        let finished = route.is_empty();
285        if finished {
286            self.routes.remove(&agent_id);
287        }
288
289        MoveResult3D {
290            position: next_pos,
291            finished,
292        }
293    }
294
295    /// Plan a route from a 3D continuous A* path.
296    pub fn plan_from_path(&mut self, agent_id: u64, waypoints: Vec<(f64, f64, f64)>) {
297        self.set_route(agent_id, waypoints);
298    }
299}
300
301/// Grid (integer) route planner for `(usize, usize)` waypoints.
302impl RoutePlanner<(usize, usize)> {
303    /// Pop and return the next grid waypoint for an agent.
304    ///
305    /// This is the grid equivalent of `move_along_route!` --
306    /// each call advances one step (one cell) along the planned path.
307    /// Returns `None` if the agent has no remaining waypoints.
308    pub fn next_waypoint(&mut self, agent_id: u64) -> Option<(usize, usize)> {
309        let route = self.routes.get_mut(&agent_id)?;
310        let wp = route.pop_front();
311        if route.is_empty() {
312            self.routes.remove(&agent_id);
313        }
314        wp
315    }
316}
317
318// ---------------------------------------------------------------------------
319// Helper functions
320// ---------------------------------------------------------------------------
321
322fn direction_2d(
323    from: (f64, f64),
324    to: (f64, f64),
325    periodic: bool,
326    extent_x: f64,
327    extent_y: f64,
328) -> (f64, f64) {
329    let mut dx = to.0 - from.0;
330    let mut dy = to.1 - from.1;
331    if periodic {
332        if dx > extent_x * 0.5 {
333            dx -= extent_x;
334        } else if dx < -extent_x * 0.5 {
335            dx += extent_x;
336        }
337        if dy > extent_y * 0.5 {
338            dy -= extent_y;
339        } else if dy < -extent_y * 0.5 {
340            dy += extent_y;
341        }
342    }
343    (dx, dy)
344}
345
346fn direction_3d(
347    from: (f64, f64, f64),
348    to: (f64, f64, f64),
349    periodic: bool,
350    extent_x: f64,
351    extent_y: f64,
352    extent_z: f64,
353) -> (f64, f64, f64) {
354    let mut dx = to.0 - from.0;
355    let mut dy = to.1 - from.1;
356    let mut dz = to.2 - from.2;
357    if periodic {
358        if dx > extent_x * 0.5 {
359            dx -= extent_x;
360        } else if dx < -extent_x * 0.5 {
361            dx += extent_x;
362        }
363        if dy > extent_y * 0.5 {
364            dy -= extent_y;
365        } else if dy < -extent_y * 0.5 {
366            dy += extent_y;
367        }
368        if dz > extent_z * 0.5 {
369            dz -= extent_z;
370        } else if dz < -extent_z * 0.5 {
371            dz += extent_z;
372        }
373    }
374    (dx, dy, dz)
375}
376
377fn normalize_pos_2d(pos: (f64, f64), extent_x: f64, extent_y: f64) -> (f64, f64) {
378    (
379        ((pos.0 % extent_x) + extent_x) % extent_x,
380        ((pos.1 % extent_y) + extent_y) % extent_y,
381    )
382}
383
384fn normalize_pos_3d(
385    pos: (f64, f64, f64),
386    extent_x: f64,
387    extent_y: f64,
388    extent_z: f64,
389) -> (f64, f64, f64) {
390    (
391        ((pos.0 % extent_x) + extent_x) % extent_x,
392        ((pos.1 % extent_y) + extent_y) % extent_y,
393        ((pos.2 % extent_z) + extent_z) % extent_z,
394    )
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn route_planner_basic() {
403        let mut planner = RoutePlanner::<(f64, f64)>::new();
404        assert!(planner.is_stationary(1));
405
406        planner.set_route(1, vec![(5.0, 5.0), (10.0, 10.0)]);
407        assert!(planner.has_route(1));
408        assert!(!planner.is_stationary(1));
409
410        planner.clear_route(1);
411        assert!(planner.is_stationary(1));
412    }
413
414    #[test]
415    fn move_along_route_exact_waypoint() {
416        let mut planner = RoutePlanner::new();
417        planner.set_route(1, vec![(10.0, 0.0)]);
418
419        // Speed=10, dt=1 -> move exactly 10 units
420        let result = planner.move_along_route_2d(1, (0.0, 0.0), 10.0, 1.0, false, 100.0, 100.0);
421        assert!((result.position.0 - 10.0).abs() < 1e-10);
422        assert!((result.position.1 - 0.0).abs() < 1e-10);
423        assert!(result.finished);
424    }
425
426    #[test]
427    fn move_along_route_partial() {
428        let mut planner = RoutePlanner::new();
429        planner.set_route(1, vec![(10.0, 0.0)]);
430
431        // Speed=5, dt=1 -> move 5 units toward (10,0) from (0,0) = (5,0)
432        let result = planner.move_along_route_2d(1, (0.0, 0.0), 5.0, 1.0, false, 100.0, 100.0);
433        assert!((result.position.0 - 5.0).abs() < 1e-10);
434        assert!(!result.finished);
435    }
436
437    #[test]
438    fn move_along_route_overshoot_chain() {
439        let mut planner = RoutePlanner::new();
440        planner.set_route(1, vec![(3.0, 0.0), (6.0, 0.0), (10.0, 0.0)]);
441
442        // Speed=5, dt=1 -> pass (3,0), continue 2 more toward (6,0) => (5,0)
443        let result = planner.move_along_route_2d(1, (0.0, 0.0), 5.0, 1.0, false, 100.0, 100.0);
444        assert!((result.position.0 - 5.0).abs() < 1e-10);
445        assert!(!result.finished);
446    }
447
448    #[test]
449    fn move_along_route_no_route() {
450        let mut planner = RoutePlanner::<(f64, f64)>::new();
451        let result = planner.move_along_route_2d(1, (5.0, 5.0), 10.0, 1.0, false, 100.0, 100.0);
452        assert!((result.position.0 - 5.0).abs() < 1e-10);
453        assert!(result.finished);
454    }
455
456    #[test]
457    fn move_along_route_3d_basic() {
458        let mut planner = RoutePlanner::new();
459        planner.set_route(1, vec![(10.0, 0.0, 0.0)]);
460
461        let result =
462            planner.move_along_route_3d(1, (0.0, 0.0, 0.0), 10.0, 1.0, false, 100.0, 100.0, 100.0);
463        assert!((result.position.0 - 10.0).abs() < 1e-10);
464        assert!(result.finished);
465    }
466
467    #[test]
468    fn grid_route_next_waypoint() {
469        let mut planner = RoutePlanner::new();
470        planner.set_route(1, vec![(1, 0), (2, 0), (3, 0)]);
471
472        assert_eq!(planner.next_waypoint(1), Some((1, 0)));
473        assert_eq!(planner.next_waypoint(1), Some((2, 0)));
474        assert_eq!(planner.next_waypoint(1), Some((3, 0)));
475        assert_eq!(planner.next_waypoint(1), None);
476        assert!(planner.is_stationary(1));
477    }
478
479    #[test]
480    fn move_along_route_2d_periodic() {
481        let mut planner = RoutePlanner::new();
482        // On a 10x10 periodic space, (9,5) to (1,5) should go through the wrap
483        planner.set_route(1, vec![(1.0, 5.0)]);
484
485        let result = planner.move_along_route_2d(1, (9.0, 5.0), 1.0, 1.0, true, 10.0, 10.0);
486        // Direction from 9 to 1 wrapping = +2, distance=2, speed=1 => move 1 unit => x=0.0
487        assert!((result.position.0).abs() < 1e-10 || (result.position.0 - 10.0).abs() < 1e-10);
488        assert!(!result.finished);
489    }
490}