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