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 RiderHasNoStop(EntityId),
59 EmptyRoute,
61 NotAnElevator(EntityId),
63 ElevatorDisabled(EntityId),
65 WrongServiceMode {
67 entity: EntityId,
69 expected: ServiceMode,
71 actual: ServiceMode,
73 },
74 NotAStop(EntityId),
76 LineNotFound(EntityId),
78 NoRoute {
80 origin: EntityId,
82 destination: EntityId,
84 origin_groups: Vec<GroupId>,
86 destination_groups: Vec<GroupId>,
88 },
89 AmbiguousRoute {
91 origin: EntityId,
93 destination: EntityId,
95 groups: Vec<GroupId>,
97 },
98 SnapshotVersion {
100 saved: String,
102 current: String,
104 },
105 SnapshotFormat(String),
107 UnresolvedCustomStrategy {
109 name: String,
111 group: GroupId,
113 },
114 MidTickSnapshot,
118}
119
120impl fmt::Display for SimError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 match self {
123 Self::InvalidConfig { field, reason } => {
124 write!(f, "invalid config '{field}': {reason}")
125 }
126 Self::EntityNotFound(id) => write!(f, "entity not found: {id:?}"),
127 Self::StopNotFound(id) => write!(f, "stop not found: {id}"),
128 Self::GroupNotFound(id) => write!(f, "group not found: {id}"),
129 Self::RouteOriginMismatch {
130 expected_origin,
131 route_origin,
132 } => {
133 write!(
134 f,
135 "route origin {route_origin:?} does not match expected origin {expected_origin:?}"
136 )
137 }
138 Self::LineDoesNotServeStop { line_or_car, stop } => {
139 write!(f, "line/car {line_or_car:?} does not serve stop {stop:?}")
140 }
141 Self::HallCallNotFound { stop, direction } => {
142 write!(f, "no hall call at stop {stop:?} direction {direction:?}")
143 }
144 Self::WrongRiderPhase {
145 rider,
146 expected,
147 actual,
148 } => {
149 write!(
150 f,
151 "rider {rider:?} is in {actual} phase, expected {expected}"
152 )
153 }
154 Self::RiderHasNoStop(id) => write!(f, "rider {id:?} has no current stop"),
155 Self::EmptyRoute => write!(f, "route has no legs"),
156 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
157 Self::ElevatorDisabled(id) => write!(f, "elevator {id:?} is disabled"),
158 Self::WrongServiceMode {
159 entity,
160 expected,
161 actual,
162 } => {
163 write!(
164 f,
165 "elevator {entity:?} is in {actual} mode, expected {expected}"
166 )
167 }
168 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
169 Self::LineNotFound(id) => write!(f, "line entity {id:?} not found"),
170 Self::NoRoute {
171 origin,
172 destination,
173 origin_groups,
174 destination_groups,
175 } => {
176 write!(
177 f,
178 "no route from {origin:?} to {destination:?} (origin served by {}, destination served by {})",
179 format_group_list(origin_groups),
180 format_group_list(destination_groups),
181 )
182 }
183 Self::AmbiguousRoute {
184 origin,
185 destination,
186 groups,
187 } => {
188 write!(
189 f,
190 "ambiguous route from {origin:?} to {destination:?}: served by groups {}",
191 format_group_list(groups),
192 )
193 }
194 Self::SnapshotVersion { saved, current } => {
195 write!(
196 f,
197 "snapshot was saved on elevator-core {saved}, but current version is {current}",
198 )
199 }
200 Self::SnapshotFormat(reason) => write!(f, "malformed snapshot: {reason}"),
201 Self::MidTickSnapshot => write!(
202 f,
203 "snapshot taken between phases of an in-progress tick; \
204 call advance_tick() before snapshot() in the substep API"
205 ),
206 Self::UnresolvedCustomStrategy { name, group } => {
207 write!(
208 f,
209 "custom dispatch strategy {name:?} for group {group} could not be resolved — \
210 provide a factory that handles this name",
211 )
212 }
213 }
214 }
215}
216
217impl std::error::Error for SimError {
218 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
219 None
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225#[non_exhaustive]
226pub enum EtaError {
227 NotAnElevator(EntityId),
229 NotAStop(EntityId),
231 StopNotQueued {
233 elevator: EntityId,
235 stop: EntityId,
237 },
238 ServiceModeExcluded(EntityId),
240 StopVanished(EntityId),
242 NoCarAssigned(EntityId),
244}
245
246impl fmt::Display for EtaError {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 match self {
249 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
250 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
251 Self::StopNotQueued { elevator, stop } => {
252 write!(f, "stop {stop:?} is not in elevator {elevator:?}'s queue")
253 }
254 Self::ServiceModeExcluded(id) => {
255 write!(f, "elevator {id:?} is in a dispatch-excluded service mode")
256 }
257 Self::StopVanished(id) => write!(f, "stop {id:?} vanished during ETA calculation"),
258 Self::NoCarAssigned(id) => {
259 write!(f, "no car assigned to serve hall call at stop {id:?}")
260 }
261 }
262 }
263}
264
265impl std::error::Error for EtaError {
266 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
267 None
268 }
269}
270
271fn format_group_list(groups: &[GroupId]) -> String {
273 if groups.is_empty() {
274 return "[]".to_string();
275 }
276 let parts: Vec<String> = groups.iter().map(GroupId::to_string).collect();
277 format!("[{}]", parts.join(", "))
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
282#[non_exhaustive]
283pub enum RejectionReason {
284 OverCapacity,
286 PreferenceBased,
288 AccessDenied,
290}
291
292impl fmt::Display for RejectionReason {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 match self {
295 Self::OverCapacity => write!(f, "over capacity"),
296 Self::PreferenceBased => write!(f, "rider preference"),
297 Self::AccessDenied => write!(f, "access denied"),
298 }
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
307pub struct RejectionContext {
308 pub attempted_weight: OrderedFloat<f64>,
310 pub current_load: OrderedFloat<f64>,
312 pub capacity: OrderedFloat<f64>,
314}
315
316impl fmt::Display for RejectionContext {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 let excess = (*self.current_load + *self.attempted_weight) - *self.capacity;
331 if excess > 0.0 {
332 write!(
333 f,
334 "over capacity by {excess:.1}kg ({:.1}/{:.1} + {:.1})",
335 *self.current_load, *self.capacity, *self.attempted_weight,
336 )
337 } else {
338 write!(
339 f,
340 "load {:.1}kg/{:.1}kg + {:.1}kg",
341 *self.current_load, *self.capacity, *self.attempted_weight,
342 )
343 }
344 }
345}
346
347impl From<EntityId> for SimError {
348 fn from(id: EntityId) -> Self {
349 Self::EntityNotFound(id)
350 }
351}
352
353impl From<StopId> for SimError {
354 fn from(id: StopId) -> Self {
355 Self::StopNotFound(id)
356 }
357}
358
359impl From<GroupId> for SimError {
360 fn from(id: GroupId) -> Self {
361 Self::GroupNotFound(id)
362 }
363}