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