1use crate::components::{
11 AccessControl, DestinationQueue, Elevator, Line, Patience, Position, Preferences, Rider, Route,
12 Stop, Velocity,
13};
14use crate::entity::EntityId;
15use crate::ids::GroupId;
16use crate::metrics::Metrics;
17use crate::stop::StopId;
18use crate::tagged_metrics::MetricTags;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct EntitySnapshot {
25 pub original_id: EntityId,
27 pub position: Option<Position>,
29 pub velocity: Option<Velocity>,
31 pub elevator: Option<Elevator>,
33 pub stop: Option<Stop>,
35 pub rider: Option<Rider>,
37 pub route: Option<Route>,
39 #[serde(default)]
41 pub line: Option<Line>,
42 pub patience: Option<Patience>,
44 pub preferences: Option<Preferences>,
46 #[serde(default)]
48 pub access_control: Option<AccessControl>,
49 pub disabled: bool,
51 #[cfg(feature = "energy")]
53 #[serde(default)]
54 pub energy_profile: Option<crate::energy::EnergyProfile>,
55 #[cfg(feature = "energy")]
57 #[serde(default)]
58 pub energy_metrics: Option<crate::energy::EnergyMetrics>,
59 #[serde(default)]
61 pub service_mode: Option<crate::components::ServiceMode>,
62 #[serde(default)]
64 pub destination_queue: Option<DestinationQueue>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct WorldSnapshot {
77 pub tick: u64,
79 pub dt: f64,
81 pub entities: Vec<EntitySnapshot>,
84 pub groups: Vec<GroupSnapshot>,
86 pub stop_lookup: HashMap<StopId, usize>,
88 pub metrics: Metrics,
90 pub metric_tags: MetricTags,
92 pub extensions: HashMap<String, HashMap<EntityId, String>>,
94 pub ticks_per_second: f64,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct LineSnapshotInfo {
101 pub entity_index: usize,
103 pub elevator_indices: Vec<usize>,
105 pub stop_indices: Vec<usize>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct GroupSnapshot {
112 pub id: GroupId,
114 pub name: String,
116 pub elevator_indices: Vec<usize>,
118 pub stop_indices: Vec<usize>,
120 pub strategy: crate::dispatch::BuiltinStrategy,
122 #[serde(default)]
124 pub lines: Vec<LineSnapshotInfo>,
125 #[serde(default)]
127 pub reposition: Option<crate::dispatch::BuiltinReposition>,
128}
129
130pub(crate) struct PendingExtensions(pub(crate) HashMap<String, HashMap<EntityId, String>>);
136
137type CustomStrategyFactory<'a> =
139 Option<&'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>>;
140
141impl WorldSnapshot {
142 #[must_use]
153 pub fn restore(
154 self,
155 custom_strategy_factory: CustomStrategyFactory<'_>,
156 ) -> crate::sim::Simulation {
157 use crate::world::{SortedStops, World};
158
159 let mut world = World::new();
160
161 let (index_to_id, id_remap) = Self::spawn_entities(&mut world, &self.entities);
163
164 Self::attach_components(&mut world, &self.entities, &index_to_id, &id_remap);
166
167 let mut sorted: Vec<(f64, EntityId)> = world
169 .iter_stops()
170 .map(|(eid, stop)| (stop.position, eid))
171 .collect();
172 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
173 world.insert_resource(SortedStops(sorted));
174
175 let (mut groups, stop_lookup, dispatchers, strategy_ids) =
177 self.rebuild_groups_and_dispatchers(&index_to_id, custom_strategy_factory);
178
179 for group in &mut groups {
182 let group_id = group.id();
183 let lines = group.lines_mut();
184 for line_info in lines.iter_mut() {
185 if line_info.entity() != EntityId::default() {
186 continue;
187 }
188 let (min_pos, max_pos) = line_info
190 .serves()
191 .iter()
192 .filter_map(|&sid| world.stop(sid).map(|s| s.position))
193 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
194 (lo.min(p), hi.max(p))
195 });
196 let line_eid = world.spawn();
197 world.set_line(
198 line_eid,
199 Line {
200 name: format!("Legacy-{group_id}"),
201 group: group_id,
202 orientation: crate::components::Orientation::Vertical,
203 position: None,
204 min_position: if min_pos.is_finite() { min_pos } else { 0.0 },
205 max_position: if max_pos.is_finite() { max_pos } else { 0.0 },
206 max_cars: None,
207 },
208 );
209 for &elev_eid in line_info.elevators() {
211 if let Some(car) = world.elevator_mut(elev_eid) {
212 car.line = line_eid;
213 }
214 }
215 line_info.set_entity(line_eid);
216 }
217 }
218
219 let remapped_exts = Self::remap_extensions(&self.extensions, &id_remap);
221 world.insert_resource(PendingExtensions(remapped_exts));
222
223 let mut tags = self.metric_tags;
225 tags.remap_entity_ids(&id_remap);
226 world.insert_resource(tags);
227
228 let mut sim = crate::sim::Simulation::from_parts(
229 world,
230 self.tick,
231 self.dt,
232 groups,
233 stop_lookup,
234 dispatchers,
235 strategy_ids,
236 self.metrics,
237 self.ticks_per_second,
238 );
239
240 for gs in &self.groups {
242 if let Some(ref repo_id) = gs.reposition
243 && let Some(strategy) = repo_id.instantiate()
244 {
245 sim.set_reposition(gs.id, strategy, repo_id.clone());
246 }
247 }
248
249 sim
250 }
251
252 fn spawn_entities(
254 world: &mut crate::world::World,
255 entities: &[EntitySnapshot],
256 ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
257 let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
258 let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
259 for snap in entities {
260 let new_id = world.spawn();
261 index_to_id.push(new_id);
262 id_remap.insert(snap.original_id, new_id);
263 }
264 (index_to_id, id_remap)
265 }
266
267 fn attach_components(
269 world: &mut crate::world::World,
270 entities: &[EntitySnapshot],
271 index_to_id: &[EntityId],
272 id_remap: &HashMap<EntityId, EntityId>,
273 ) {
274 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
275 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
276
277 for (i, snap) in entities.iter().enumerate() {
278 let eid = index_to_id[i];
279
280 if let Some(pos) = snap.position {
281 world.set_position(eid, pos);
282 }
283 if let Some(vel) = snap.velocity {
284 world.set_velocity(eid, vel);
285 }
286 if let Some(ref elev) = snap.elevator {
287 let mut e = elev.clone();
288 e.riders = e.riders.iter().map(|&r| remap(r)).collect();
289 e.target_stop = remap_opt(e.target_stop);
290 e.line = remap(e.line);
291 e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
292 e.phase = match e.phase {
293 crate::components::ElevatorPhase::MovingToStop(s) => {
294 crate::components::ElevatorPhase::MovingToStop(remap(s))
295 }
296 crate::components::ElevatorPhase::Repositioning(s) => {
297 crate::components::ElevatorPhase::Repositioning(remap(s))
298 }
299 other => other,
300 };
301 world.set_elevator(eid, e);
302 }
303 if let Some(ref stop) = snap.stop {
304 world.set_stop(eid, stop.clone());
305 }
306 if let Some(ref rider) = snap.rider {
307 use crate::components::RiderPhase;
308 let mut r = rider.clone();
309 r.current_stop = remap_opt(r.current_stop);
310 r.phase = match r.phase {
311 RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
312 RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
313 RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
314 other => other,
315 };
316 world.set_rider(eid, r);
317 }
318 if let Some(ref route) = snap.route {
319 let mut rt = route.clone();
320 for leg in &mut rt.legs {
321 leg.from = remap(leg.from);
322 leg.to = remap(leg.to);
323 if let crate::components::TransportMode::Line(ref mut l) = leg.via {
324 *l = remap(*l);
325 }
326 }
327 world.set_route(eid, rt);
328 }
329 if let Some(ref line) = snap.line {
330 world.set_line(eid, line.clone());
331 }
332 if let Some(patience) = snap.patience {
333 world.set_patience(eid, patience);
334 }
335 if let Some(prefs) = snap.preferences {
336 world.set_preferences(eid, prefs);
337 }
338 if let Some(ref ac) = snap.access_control {
339 let remapped =
340 AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
341 world.set_access_control(eid, remapped);
342 }
343 if snap.disabled {
344 world.disable(eid);
345 }
346 #[cfg(feature = "energy")]
347 if let Some(ref profile) = snap.energy_profile {
348 world.set_energy_profile(eid, profile.clone());
349 }
350 #[cfg(feature = "energy")]
351 if let Some(ref em) = snap.energy_metrics {
352 world.set_energy_metrics(eid, em.clone());
353 }
354 if let Some(mode) = snap.service_mode {
355 world.set_service_mode(eid, mode);
356 }
357 if let Some(ref dq) = snap.destination_queue {
358 use crate::components::DestinationQueue as DQ;
359 let mut new_dq = DQ::new();
360 for &e in dq.queue() {
361 new_dq.push_back(remap(e));
362 }
363 world.set_destination_queue(eid, new_dq);
364 }
365 }
366 }
367
368 #[allow(clippy::type_complexity)]
370 fn rebuild_groups_and_dispatchers(
371 &self,
372 index_to_id: &[EntityId],
373 custom_strategy_factory: CustomStrategyFactory<'_>,
374 ) -> (
375 Vec<crate::dispatch::ElevatorGroup>,
376 HashMap<StopId, EntityId>,
377 std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
378 std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
379 ) {
380 use crate::dispatch::ElevatorGroup;
381
382 let groups: Vec<ElevatorGroup> = self
383 .groups
384 .iter()
385 .map(|gs| {
386 let elevator_entities: Vec<EntityId> = gs
387 .elevator_indices
388 .iter()
389 .filter_map(|&i| index_to_id.get(i).copied())
390 .collect();
391 let stop_entities: Vec<EntityId> = gs
392 .stop_indices
393 .iter()
394 .filter_map(|&i| index_to_id.get(i).copied())
395 .collect();
396
397 let lines = if gs.lines.is_empty() {
398 vec![crate::dispatch::LineInfo::new(
401 EntityId::default(),
402 elevator_entities,
403 stop_entities,
404 )]
405 } else {
406 gs.lines
407 .iter()
408 .filter_map(|lsi| {
409 let entity = index_to_id.get(lsi.entity_index).copied()?;
410 Some(crate::dispatch::LineInfo::new(
411 entity,
412 lsi.elevator_indices
413 .iter()
414 .filter_map(|&i| index_to_id.get(i).copied())
415 .collect(),
416 lsi.stop_indices
417 .iter()
418 .filter_map(|&i| index_to_id.get(i).copied())
419 .collect(),
420 ))
421 })
422 .collect()
423 };
424
425 ElevatorGroup::new(gs.id, gs.name.clone(), lines)
426 })
427 .collect();
428
429 let stop_lookup: HashMap<StopId, EntityId> = self
430 .stop_lookup
431 .iter()
432 .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
433 .collect();
434
435 let mut dispatchers = std::collections::BTreeMap::new();
436 let mut strategy_ids = std::collections::BTreeMap::new();
437 for (gs, group) in self.groups.iter().zip(groups.iter()) {
438 let strategy: Box<dyn crate::dispatch::DispatchStrategy> = gs
439 .strategy
440 .instantiate()
441 .or_else(|| {
442 if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
443 custom_strategy_factory.and_then(|f| f(name))
444 } else {
445 None
446 }
447 })
448 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
449 dispatchers.insert(group.id(), strategy);
450 strategy_ids.insert(group.id(), gs.strategy.clone());
451 }
452
453 (groups, stop_lookup, dispatchers, strategy_ids)
454 }
455
456 fn remap_extensions(
458 extensions: &HashMap<String, HashMap<EntityId, String>>,
459 id_remap: &HashMap<EntityId, EntityId>,
460 ) -> HashMap<String, HashMap<EntityId, String>> {
461 extensions
462 .iter()
463 .map(|(name, entries)| {
464 let remapped: HashMap<EntityId, String> = entries
465 .iter()
466 .map(|(old_id, data)| {
467 let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
468 (new_id, data.clone())
469 })
470 .collect();
471 (name.clone(), remapped)
472 })
473 .collect()
474 }
475}
476
477impl crate::sim::Simulation {
478 #[must_use]
484 pub fn snapshot(&self) -> WorldSnapshot {
485 let world = self.world();
486
487 let all_ids: Vec<EntityId> = world.alive.keys().collect();
489 let id_to_index: HashMap<EntityId, usize> = all_ids
490 .iter()
491 .copied()
492 .enumerate()
493 .map(|(i, e)| (e, i))
494 .collect();
495
496 let entities: Vec<EntitySnapshot> = all_ids
498 .iter()
499 .map(|&eid| EntitySnapshot {
500 original_id: eid,
501 position: world.position(eid).copied(),
502 velocity: world.velocity(eid).copied(),
503 elevator: world.elevator(eid).cloned(),
504 stop: world.stop(eid).cloned(),
505 rider: world.rider(eid).cloned(),
506 route: world.route(eid).cloned(),
507 line: world.line(eid).cloned(),
508 patience: world.patience(eid).copied(),
509 preferences: world.preferences(eid).copied(),
510 access_control: world.access_control(eid).cloned(),
511 disabled: world.is_disabled(eid),
512 #[cfg(feature = "energy")]
513 energy_profile: world.energy_profile(eid).cloned(),
514 #[cfg(feature = "energy")]
515 energy_metrics: world.energy_metrics(eid).cloned(),
516 service_mode: world.service_mode(eid).copied(),
517 destination_queue: world.destination_queue(eid).cloned(),
518 })
519 .collect();
520
521 let groups: Vec<GroupSnapshot> = self
523 .groups()
524 .iter()
525 .map(|g| {
526 let lines: Vec<LineSnapshotInfo> = g
527 .lines()
528 .iter()
529 .filter_map(|li| {
530 let entity_index = id_to_index.get(&li.entity()).copied()?;
531 Some(LineSnapshotInfo {
532 entity_index,
533 elevator_indices: li
534 .elevators()
535 .iter()
536 .filter_map(|eid| id_to_index.get(eid).copied())
537 .collect(),
538 stop_indices: li
539 .serves()
540 .iter()
541 .filter_map(|eid| id_to_index.get(eid).copied())
542 .collect(),
543 })
544 })
545 .collect();
546 GroupSnapshot {
547 id: g.id(),
548 name: g.name().to_owned(),
549 elevator_indices: g
550 .elevator_entities()
551 .iter()
552 .filter_map(|eid| id_to_index.get(eid).copied())
553 .collect(),
554 stop_indices: g
555 .stop_entities()
556 .iter()
557 .filter_map(|eid| id_to_index.get(eid).copied())
558 .collect(),
559 strategy: self
560 .strategy_id(g.id())
561 .cloned()
562 .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
563 lines,
564 reposition: self.reposition_id(g.id()).cloned(),
565 }
566 })
567 .collect();
568
569 let stop_lookup: HashMap<StopId, usize> = self
571 .stop_lookup_iter()
572 .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
573 .collect();
574
575 WorldSnapshot {
576 tick: self.current_tick(),
577 dt: self.dt(),
578 entities,
579 groups,
580 stop_lookup,
581 metrics: self.metrics().clone(),
582 metric_tags: self
583 .world()
584 .resource::<MetricTags>()
585 .cloned()
586 .unwrap_or_default(),
587 extensions: self.world().serialize_extensions(),
588 ticks_per_second: 1.0 / self.dt(),
589 }
590 }
591}