Skip to main content

elevator_core/sim/
destinations.rs

1//! Imperative destination queue API (push/clear/abort).
2//!
3//! Part of the [`super::Simulation`] API surface; extracted from the
4//! monolithic `sim.rs` for readability. See the parent module for the
5//! overarching essential-API summary.
6
7use crate::components::ElevatorPhase;
8use crate::entity::{ElevatorId, EntityId};
9use crate::error::SimError;
10use crate::events::Event;
11use crate::stop::StopRef;
12use crate::world::World;
13
14impl super::Simulation {
15    // ── Destination queue (imperative dispatch) ────────────────────
16
17    /// Read-only view of an elevator's destination queue (FIFO of target
18    /// stop `EntityId`s).
19    ///
20    /// Returns `None` if `elev` is not an elevator entity. Returns
21    /// `Some(&[])` for elevators with an empty queue.
22    #[must_use]
23    pub fn destination_queue(&self, elev: ElevatorId) -> Option<&[EntityId]> {
24        let elev = elev.entity();
25        self.world
26            .destination_queue(elev)
27            .map(crate::components::DestinationQueue::queue)
28    }
29
30    /// Push a stop onto the back of an elevator's destination queue.
31    ///
32    /// Adjacent duplicates are suppressed: if the last entry already equals
33    /// `stop`, the queue is unchanged and no event is emitted.
34    /// Otherwise emits [`Event::DestinationQueued`].
35    ///
36    /// # Errors
37    ///
38    /// - [`SimError::NotAnElevator`] if `elev` is not an elevator.
39    /// - [`SimError::NotAStop`] if `stop` is not a stop.
40    pub fn push_destination(
41        &mut self,
42        elev: ElevatorId,
43        stop: impl Into<StopRef>,
44    ) -> Result<(), SimError> {
45        let elev = elev.entity();
46        let stop = self.resolve_stop(stop.into())?;
47        self.validate_push_targets(elev, stop)?;
48        let appended = self
49            .world
50            .destination_queue_mut(elev)
51            .is_some_and(|q| q.push_back(stop));
52        if appended {
53            self.events.emit(Event::DestinationQueued {
54                elevator: elev,
55                stop,
56                tick: self.tick,
57            });
58        }
59        Ok(())
60    }
61
62    /// Insert a stop at the front of an elevator's destination queue —
63    /// "go here next, before anything else in the queue".
64    ///
65    /// On the next `AdvanceQueue` phase (between Dispatch and Movement),
66    /// the elevator redirects to this new front if it differs from the
67    /// current target.
68    ///
69    /// Adjacent duplicates are suppressed: if the first entry already equals
70    /// `stop`, the queue is unchanged and no event is emitted.
71    ///
72    /// # Errors
73    ///
74    /// - [`SimError::NotAnElevator`] if `elev` is not an elevator.
75    /// - [`SimError::NotAStop`] if `stop` is not a stop.
76    pub fn push_destination_front(
77        &mut self,
78        elev: ElevatorId,
79        stop: impl Into<StopRef>,
80    ) -> Result<(), SimError> {
81        let elev = elev.entity();
82        let stop = self.resolve_stop(stop.into())?;
83        self.validate_push_targets(elev, stop)?;
84        let inserted = self
85            .world
86            .destination_queue_mut(elev)
87            .is_some_and(|q| q.push_front(stop));
88        if inserted {
89            self.events.emit(Event::DestinationQueued {
90                elevator: elev,
91                stop,
92                tick: self.tick,
93            });
94        }
95        Ok(())
96    }
97
98    /// Clear an elevator's destination queue.
99    ///
100    /// Does **not** affect an in-flight movement — the elevator will
101    /// finish its current leg and then go idle (since the queue is empty).
102    /// To stop a moving car immediately, use
103    /// [`abort_movement`](Self::abort_movement), which brakes the car to
104    /// the nearest reachable stop and also clears the queue.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`SimError::NotAnElevator`] if `elev` is not an elevator.
109    pub fn clear_destinations(&mut self, elev: ElevatorId) -> Result<(), SimError> {
110        let elev = elev.entity();
111        if self.world.elevator(elev).is_none() {
112            return Err(SimError::NotAnElevator(elev));
113        }
114        if let Some(q) = self.world.destination_queue_mut(elev) {
115            q.clear();
116        }
117        Ok(())
118    }
119
120    /// Abort the elevator's in-flight movement and park at the nearest
121    /// reachable stop.
122    ///
123    /// Computes the minimum stopping position under the car's normal
124    /// deceleration profile (see
125    /// [`future_stop_position`](Self::future_stop_position)), picks the
126    /// closest stop at or past that position in the current direction of
127    /// travel, re-targets there via
128    /// [`ElevatorPhase::Repositioning`](crate::components::ElevatorPhase)
129    /// so the car arrives **without opening doors**, and clears any queued
130    /// destinations. Onboard riders stay aboard.
131    ///
132    /// Emits [`Event::MovementAborted`](crate::events::Event)
133    /// when an abort occurs.
134    ///
135    /// # No-op conditions
136    ///
137    /// Returns `Ok(())` without changes if the car is not currently moving
138    /// (any phase other than
139    /// [`MovingToStop`](crate::components::ElevatorPhase::MovingToStop) or
140    /// [`Repositioning`](crate::components::ElevatorPhase::Repositioning)),
141    /// or if the simulation has no stops.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`SimError::NotAnElevator`] if `elev` is not an elevator.
146    pub fn abort_movement(&mut self, elev: ElevatorId) -> Result<(), SimError> {
147        let eid = elev.entity();
148        let Some(car) = self.world.elevator(eid) else {
149            return Err(SimError::NotAnElevator(eid));
150        };
151        if !car.phase().is_moving() {
152            return Ok(());
153        }
154
155        let pos = self.world.position(eid).map_or(0.0, |p| p.value);
156        let vel = self.world.velocity(eid).map_or(0.0, |v| v.value);
157        let Some(brake_pos) = self.future_stop_position(eid) else {
158            return Ok(());
159        };
160
161        let Some(brake_stop) = brake_target_stop(&self.world, pos, vel, brake_pos) else {
162            return Ok(());
163        };
164
165        if let Some(car) = self.world.elevator_mut(eid) {
166            car.phase = ElevatorPhase::Repositioning(brake_stop);
167            car.target_stop = Some(brake_stop);
168            car.repositioning = true;
169        }
170        if let Some(q) = self.world.destination_queue_mut(eid) {
171            q.clear();
172        }
173
174        self.events.emit(Event::MovementAborted {
175            elevator: eid,
176            brake_target: brake_stop,
177            tick: self.tick,
178        });
179
180        Ok(())
181    }
182
183    /// Recall an elevator to a specific stop.
184    ///
185    /// Clears the destination queue and replaces it with `stop` as the
186    /// sole target. Riders aboard stay aboard. The car proceeds to the
187    /// recall stop and opens doors on arrival (standard `MovingToStop`
188    /// semantics).
189    ///
190    /// If the car is already at the recall stop, doors open on the next
191    /// tick. If the car is mid-flight, it finishes its current leg's
192    /// deceleration (the movement system handles the redirect). If the
193    /// car is in a door cycle (opening/loading/closing), the cycle
194    /// completes, then the car departs.
195    ///
196    /// Works in any service mode — even dispatch-excluded cars can be
197    /// recalled. Emits [`Event::ElevatorRecalled`].
198    ///
199    /// # Service mode interaction
200    ///
201    /// Non-Normal modes suppress the loading phase (both boarding and
202    /// exiting). If you set `OutOfService` *before* the car arrives at
203    /// the recall stop, riders aboard will not auto-exit. For the
204    /// fire-service pattern, switch to `OutOfService` only *after*
205    /// observing `ElevatorArrived` and letting the loading phase drain.
206    ///
207    /// # Errors
208    ///
209    /// - [`SimError::NotAnElevator`] if `elev` is not an elevator.
210    /// - [`SimError::NotAStop`] if `stop` is not a stop.
211    pub fn recall_to(
212        &mut self,
213        elev: ElevatorId,
214        stop: impl Into<StopRef>,
215    ) -> Result<(), SimError> {
216        let eid = elev.entity();
217        let stop = self.resolve_stop(stop.into())?;
218        self.validate_push_targets(eid, stop)?;
219
220        if let Some(q) = self.world.destination_queue_mut(eid) {
221            q.clear();
222            q.push_back(stop);
223        }
224
225        self.events.emit(Event::ElevatorRecalled {
226            elevator: eid,
227            to_stop: stop,
228            tick: self.tick,
229        });
230
231        Ok(())
232    }
233
234    /// Validate that `elev` is an elevator and `stop` is a stop.
235    fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
236        if self.world.elevator(elev).is_none() {
237            return Err(SimError::NotAnElevator(elev));
238        }
239        if self.world.stop(stop).is_none() {
240            return Err(SimError::NotAStop(stop));
241        }
242        Ok(())
243    }
244}
245
246/// Pick the stop to park at when aborting an in-flight movement.
247///
248/// Tries three strategies in order:
249///
250/// 1. **Closest stop at or past `brake_pos` in the direction of travel.**
251///    The car can decelerate into it naturally without overshoot.
252/// 2. **Farthest stop still in the direction of travel (end-of-line).**
253///    If the car is too close to the end of the line to fit a full
254///    deceleration, pick the terminal stop ahead of it; the movement
255///    system's overshoot-snap will absorb the small residual distance.
256/// 3. **Nearest stop overall.** Only reachable when the car has no
257///    stops ahead of it at all (e.g., single-stop worlds or the car is
258///    already past the final stop); also handles the `vel == 0` case.
259///
260/// Returns `None` only if the world has no stops at all.
261fn brake_target_stop(world: &World, pos: f64, vel: f64, brake_pos: f64) -> Option<EntityId> {
262    let dir = vel.signum();
263    if dir != 0.0 {
264        let ahead_of_brake = world
265            .iter_stops()
266            .filter(|(_, stop)| (stop.position() - brake_pos) * dir >= 0.0)
267            .min_by(|(_, a), (_, b)| {
268                (a.position() - pos)
269                    .abs()
270                    .total_cmp(&(b.position() - pos).abs())
271            })
272            .map(|(id, _)| id);
273        if ahead_of_brake.is_some() {
274            return ahead_of_brake;
275        }
276        let ahead_of_car = world
277            .iter_stops()
278            .filter(|(_, stop)| (stop.position() - pos) * dir >= 0.0)
279            .max_by(|(_, a), (_, b)| {
280                ((a.position() - pos) * dir).total_cmp(&((b.position() - pos) * dir))
281            })
282            .map(|(id, _)| id);
283        if ahead_of_car.is_some() {
284            return ahead_of_car;
285        }
286    }
287    world.find_nearest_stop(brake_pos)
288}