1use crate::components::{CallDirection, RiderPhaseKind, ServiceMode};
4use crate::entity::EntityId;
5use crate::ids::GroupId;
6use crate::stop::StopId;
7use ordered_float::OrderedFloat;
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum SimError {
14 InvalidConfig {
16 field: &'static str,
18 reason: String,
20 },
21 EntityNotFound(EntityId),
23 StopNotFound(StopId),
25 GroupNotFound(GroupId),
27 RouteOriginMismatch {
29 expected_origin: EntityId,
31 route_origin: EntityId,
33 },
34 LineDoesNotServeStop {
36 line_or_car: EntityId,
38 stop: EntityId,
40 },
41 HallCallNotFound {
43 stop: EntityId,
45 direction: CallDirection,
47 },
48 WrongRiderPhase {
50 rider: EntityId,
52 expected: RiderPhaseKind,
54 actual: RiderPhaseKind,
56 },
57 IllegalTransition {
63 rider: EntityId,
65 from: RiderPhaseKind,
67 to: RiderPhaseKind,
69 },
70 RiderHasNoStop(EntityId),
72 EmptyRoute,
74 NotAnElevator(EntityId),
76 ElevatorDisabled(EntityId),
78 WrongServiceMode {
80 entity: EntityId,
82 expected: ServiceMode,
84 actual: ServiceMode,
86 },
87 NotAStop(EntityId),
89 LineNotFound(EntityId),
91 NoRoute {
93 origin: EntityId,
95 destination: EntityId,
97 origin_groups: Vec<GroupId>,
99 destination_groups: Vec<GroupId>,
101 },
102 AmbiguousRoute {
104 origin: EntityId,
106 destination: EntityId,
108 groups: Vec<GroupId>,
110 },
111 SnapshotVersion {
113 saved: String,
115 current: String,
117 },
118 SnapshotFormat(String),
120 UnresolvedCustomStrategy {
122 name: String,
124 group: GroupId,
126 },
127 MidTickSnapshot,
131}
132
133impl fmt::Display for SimError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::InvalidConfig { field, reason } => {
137 write!(f, "invalid config '{field}': {reason}")
138 }
139 Self::EntityNotFound(id) => write!(f, "entity not found: {id:?}"),
140 Self::StopNotFound(id) => write!(f, "stop not found: {id}"),
141 Self::GroupNotFound(id) => write!(f, "group not found: {id}"),
142 Self::RouteOriginMismatch {
143 expected_origin,
144 route_origin,
145 } => {
146 write!(
147 f,
148 "route origin {route_origin:?} does not match expected origin {expected_origin:?}"
149 )
150 }
151 Self::LineDoesNotServeStop { line_or_car, stop } => {
152 write!(f, "line/car {line_or_car:?} does not serve stop {stop:?}")
153 }
154 Self::HallCallNotFound { stop, direction } => {
155 write!(f, "no hall call at stop {stop:?} direction {direction:?}")
156 }
157 Self::WrongRiderPhase {
158 rider,
159 expected,
160 actual,
161 } => {
162 write!(
163 f,
164 "rider {rider:?} is in {actual} phase, expected {expected}"
165 )
166 }
167 Self::IllegalTransition { rider, from, to } => {
168 write!(
169 f,
170 "rider {rider:?} cannot transition {from} -> {to}: not in legality matrix"
171 )
172 }
173 Self::RiderHasNoStop(id) => write!(f, "rider {id:?} has no current stop"),
174 Self::EmptyRoute => write!(f, "route has no legs"),
175 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
176 Self::ElevatorDisabled(id) => write!(f, "elevator {id:?} is disabled"),
177 Self::WrongServiceMode {
178 entity,
179 expected,
180 actual,
181 } => {
182 write!(
183 f,
184 "elevator {entity:?} is in {actual} mode, expected {expected}"
185 )
186 }
187 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
188 Self::LineNotFound(id) => write!(f, "line entity {id:?} not found"),
189 Self::NoRoute {
190 origin,
191 destination,
192 origin_groups,
193 destination_groups,
194 } => {
195 write!(
196 f,
197 "no route from {origin:?} to {destination:?} (origin served by {}, destination served by {})",
198 format_group_list(origin_groups),
199 format_group_list(destination_groups),
200 )
201 }
202 Self::AmbiguousRoute {
203 origin,
204 destination,
205 groups,
206 } => {
207 write!(
208 f,
209 "ambiguous route from {origin:?} to {destination:?}: served by groups {}",
210 format_group_list(groups),
211 )
212 }
213 Self::SnapshotVersion { saved, current } => {
214 write!(
215 f,
216 "snapshot was saved on elevator-core {saved}, but current version is {current}",
217 )
218 }
219 Self::SnapshotFormat(reason) => write!(f, "malformed snapshot: {reason}"),
220 Self::MidTickSnapshot => write!(
221 f,
222 "snapshot taken between phases of an in-progress tick; \
223 call advance_tick() before snapshot() in the substep API"
224 ),
225 Self::UnresolvedCustomStrategy { name, group } => {
226 write!(
227 f,
228 "custom dispatch strategy {name:?} for group {group} could not be resolved — \
229 provide a factory that handles this name",
230 )
231 }
232 }
233 }
234}
235
236impl std::error::Error for SimError {
237 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
238 None
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244#[non_exhaustive]
245pub enum EtaError {
246 NotAnElevator(EntityId),
248 NotAStop(EntityId),
250 StopNotQueued {
252 elevator: EntityId,
254 stop: EntityId,
256 },
257 ServiceModeExcluded(EntityId),
259 StopVanished(EntityId),
261 NoCarAssigned(EntityId),
263}
264
265impl fmt::Display for EtaError {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 match self {
268 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
269 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
270 Self::StopNotQueued { elevator, stop } => {
271 write!(f, "stop {stop:?} is not in elevator {elevator:?}'s queue")
272 }
273 Self::ServiceModeExcluded(id) => {
274 write!(f, "elevator {id:?} is in a dispatch-excluded service mode")
275 }
276 Self::StopVanished(id) => write!(f, "stop {id:?} vanished during ETA calculation"),
277 Self::NoCarAssigned(id) => {
278 write!(f, "no car assigned to serve hall call at stop {id:?}")
279 }
280 }
281 }
282}
283
284impl std::error::Error for EtaError {
285 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
286 None
287 }
288}
289
290fn format_group_list(groups: &[GroupId]) -> String {
292 if groups.is_empty() {
293 return "[]".to_string();
294 }
295 let parts: Vec<String> = groups.iter().map(GroupId::to_string).collect();
296 format!("[{}]", parts.join(", "))
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
301#[non_exhaustive]
302pub enum RejectionReason {
303 OverCapacity,
305 PreferenceBased,
307 AccessDenied,
309}
310
311impl fmt::Display for RejectionReason {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 match self {
314 Self::OverCapacity => write!(f, "over capacity"),
315 Self::PreferenceBased => write!(f, "rider preference"),
316 Self::AccessDenied => write!(f, "access denied"),
317 }
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
326pub struct RejectionContext {
327 pub attempted_weight: OrderedFloat<f64>,
329 pub current_load: OrderedFloat<f64>,
331 pub capacity: OrderedFloat<f64>,
333}
334
335impl fmt::Display for RejectionContext {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 let excess = (*self.current_load + *self.attempted_weight) - *self.capacity;
350 if excess > 0.0 {
351 write!(
352 f,
353 "over capacity by {excess:.1}kg ({:.1}/{:.1} + {:.1})",
354 *self.current_load, *self.capacity, *self.attempted_weight,
355 )
356 } else {
357 write!(
358 f,
359 "load {:.1}kg/{:.1}kg + {:.1}kg",
360 *self.current_load, *self.capacity, *self.attempted_weight,
361 )
362 }
363 }
364}
365
366impl From<EntityId> for SimError {
367 fn from(id: EntityId) -> Self {
368 Self::EntityNotFound(id)
369 }
370}
371
372impl From<StopId> for SimError {
373 fn from(id: StopId) -> Self {
374 Self::StopNotFound(id)
375 }
376}
377
378impl From<GroupId> for SimError {
379 fn from(id: GroupId) -> Self {
380 Self::GroupNotFound(id)
381 }
382}