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}
108
109impl fmt::Display for SimError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Self::InvalidConfig { field, reason } => {
113 write!(f, "invalid config '{field}': {reason}")
114 }
115 Self::EntityNotFound(id) => write!(f, "entity not found: {id:?}"),
116 Self::StopNotFound(id) => write!(f, "stop not found: {id}"),
117 Self::GroupNotFound(id) => write!(f, "group not found: {id}"),
118 Self::RouteOriginMismatch {
119 expected_origin,
120 route_origin,
121 } => {
122 write!(
123 f,
124 "route origin {route_origin:?} does not match expected origin {expected_origin:?}"
125 )
126 }
127 Self::LineDoesNotServeStop { line_or_car, stop } => {
128 write!(f, "line/car {line_or_car:?} does not serve stop {stop:?}")
129 }
130 Self::HallCallNotFound { stop, direction } => {
131 write!(f, "no hall call at stop {stop:?} direction {direction:?}")
132 }
133 Self::WrongRiderPhase {
134 rider,
135 expected,
136 actual,
137 } => {
138 write!(
139 f,
140 "rider {rider:?} is in {actual} phase, expected {expected}"
141 )
142 }
143 Self::RiderHasNoStop(id) => write!(f, "rider {id:?} has no current stop"),
144 Self::EmptyRoute => write!(f, "route has no legs"),
145 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
146 Self::ElevatorDisabled(id) => write!(f, "elevator {id:?} is disabled"),
147 Self::WrongServiceMode {
148 entity,
149 expected,
150 actual,
151 } => {
152 write!(
153 f,
154 "elevator {entity:?} is in {actual} mode, expected {expected}"
155 )
156 }
157 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
158 Self::LineNotFound(id) => write!(f, "line entity {id:?} not found"),
159 Self::NoRoute {
160 origin,
161 destination,
162 origin_groups,
163 destination_groups,
164 } => {
165 write!(
166 f,
167 "no route from {origin:?} to {destination:?} (origin served by {}, destination served by {})",
168 format_group_list(origin_groups),
169 format_group_list(destination_groups),
170 )
171 }
172 Self::AmbiguousRoute {
173 origin,
174 destination,
175 groups,
176 } => {
177 write!(
178 f,
179 "ambiguous route from {origin:?} to {destination:?}: served by groups {}",
180 format_group_list(groups),
181 )
182 }
183 Self::SnapshotVersion { saved, current } => {
184 write!(
185 f,
186 "snapshot was saved on elevator-core {saved}, but current version is {current}",
187 )
188 }
189 Self::SnapshotFormat(reason) => write!(f, "malformed snapshot: {reason}"),
190 }
191 }
192}
193
194impl std::error::Error for SimError {
195 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
196 None
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202#[non_exhaustive]
203pub enum EtaError {
204 NotAnElevator(EntityId),
206 NotAStop(EntityId),
208 StopNotQueued {
210 elevator: EntityId,
212 stop: EntityId,
214 },
215 ServiceModeExcluded(EntityId),
217 StopVanished(EntityId),
219 NoCarAssigned(EntityId),
221}
222
223impl fmt::Display for EtaError {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 match self {
226 Self::NotAnElevator(id) => write!(f, "entity {id:?} is not an elevator"),
227 Self::NotAStop(id) => write!(f, "entity {id:?} is not a stop"),
228 Self::StopNotQueued { elevator, stop } => {
229 write!(f, "stop {stop:?} is not in elevator {elevator:?}'s queue")
230 }
231 Self::ServiceModeExcluded(id) => {
232 write!(f, "elevator {id:?} is in a dispatch-excluded service mode")
233 }
234 Self::StopVanished(id) => write!(f, "stop {id:?} vanished during ETA calculation"),
235 Self::NoCarAssigned(id) => {
236 write!(f, "no car assigned to serve hall call at stop {id:?}")
237 }
238 }
239 }
240}
241
242impl std::error::Error for EtaError {
243 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
244 None
245 }
246}
247
248fn format_group_list(groups: &[GroupId]) -> String {
250 if groups.is_empty() {
251 return "[]".to_string();
252 }
253 let parts: Vec<String> = groups.iter().map(GroupId::to_string).collect();
254 format!("[{}]", parts.join(", "))
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
259#[non_exhaustive]
260pub enum RejectionReason {
261 OverCapacity,
263 PreferenceBased,
265 AccessDenied,
267}
268
269impl fmt::Display for RejectionReason {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match self {
272 Self::OverCapacity => write!(f, "over capacity"),
273 Self::PreferenceBased => write!(f, "rider preference"),
274 Self::AccessDenied => write!(f, "access denied"),
275 }
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
284pub struct RejectionContext {
285 pub attempted_weight: OrderedFloat<f64>,
287 pub current_load: OrderedFloat<f64>,
289 pub capacity: OrderedFloat<f64>,
291}
292
293impl fmt::Display for RejectionContext {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 let excess = (*self.current_load + *self.attempted_weight) - *self.capacity;
308 if excess > 0.0 {
309 write!(
310 f,
311 "over capacity by {excess:.1}kg ({:.1}/{:.1} + {:.1})",
312 *self.current_load, *self.capacity, *self.attempted_weight,
313 )
314 } else {
315 write!(
316 f,
317 "load {:.1}kg/{:.1}kg + {:.1}kg",
318 *self.current_load, *self.capacity, *self.attempted_weight,
319 )
320 }
321 }
322}
323
324impl From<EntityId> for SimError {
325 fn from(id: EntityId) -> Self {
326 Self::EntityNotFound(id)
327 }
328}
329
330impl From<StopId> for SimError {
331 fn from(id: StopId) -> Self {
332 Self::StopNotFound(id)
333 }
334}
335
336impl From<GroupId> for SimError {
337 fn from(id: GroupId) -> Self {
338 Self::GroupNotFound(id)
339 }
340}