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(elev) 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}