1use crate::components::Route;
8use crate::components::{Elevator, ElevatorPhase, Line, Position, Stop, Velocity};
9use crate::dispatch::{BuiltinStrategy, DispatchStrategy, ElevatorGroup, LineInfo};
10use crate::door::DoorState;
11use crate::entity::EntityId;
12use crate::error::SimError;
13use crate::events::Event;
14use crate::ids::GroupId;
15use crate::topology::TopologyGraph;
16
17use super::{ElevatorParams, LineParams, Simulation};
18
19impl Simulation {
20 pub(super) fn mark_topo_dirty(&self) {
24 if let Ok(mut g) = self.topo_graph.lock() {
25 g.mark_dirty();
26 }
27 }
28
29 pub(super) fn find_line(&self, line: EntityId) -> Result<(usize, usize), SimError> {
31 self.groups
32 .iter()
33 .enumerate()
34 .find_map(|(gi, g)| {
35 g.lines()
36 .iter()
37 .position(|li| li.entity() == line)
38 .map(|li_idx| (gi, li_idx))
39 })
40 .ok_or(SimError::LineNotFound(line))
41 }
42
43 pub fn add_stop(
53 &mut self,
54 name: String,
55 position: f64,
56 line: EntityId,
57 ) -> Result<EntityId, SimError> {
58 if !position.is_finite() {
59 return Err(SimError::InvalidConfig {
60 field: "position",
61 reason: format!(
62 "stop position must be finite (got {position}); NaN/±inf \
63 corrupt SortedStops ordering and find_stop_at_position lookup"
64 ),
65 });
66 }
67
68 let group_id = self
69 .world
70 .line(line)
71 .map(|l| l.group)
72 .ok_or(SimError::LineNotFound(line))?;
73
74 let (group_idx, line_idx) = self.find_line(line)?;
75
76 let eid = self.world.spawn();
77 self.world.set_stop(eid, Stop { name, position });
78 self.world.set_position(eid, Position { value: position });
79
80 self.groups[group_idx].lines_mut()[line_idx]
82 .serves_mut()
83 .push(eid);
84
85 self.groups[group_idx].push_stop(eid);
87
88 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
90 let idx = sorted.0.partition_point(|&(p, _)| p < position);
91 sorted.0.insert(idx, (position, eid));
92 }
93
94 self.mark_topo_dirty();
95 self.events.emit(Event::StopAdded {
96 stop: eid,
97 line,
98 group: group_id,
99 tick: self.tick,
100 });
101 Ok(eid)
102 }
103
104 pub fn add_elevator(
110 &mut self,
111 params: &ElevatorParams,
112 line: EntityId,
113 starting_position: f64,
114 ) -> Result<EntityId, SimError> {
115 let group_id = self
116 .world
117 .line(line)
118 .map(|l| l.group)
119 .ok_or(SimError::LineNotFound(line))?;
120
121 let (group_idx, line_idx) = self.find_line(line)?;
122
123 if let Some(max) = self.world.line(line).and_then(Line::max_cars) {
125 let current_count = self.groups[group_idx].lines()[line_idx].elevators().len();
126 if current_count >= max {
127 return Err(SimError::InvalidConfig {
128 field: "line.max_cars",
129 reason: format!("line already has {current_count} cars (max {max})"),
130 });
131 }
132 }
133
134 let eid = self.world.spawn();
135 self.world.set_position(
136 eid,
137 Position {
138 value: starting_position,
139 },
140 );
141 self.world.set_velocity(eid, Velocity { value: 0.0 });
142 self.world.set_elevator(
143 eid,
144 Elevator {
145 phase: ElevatorPhase::Idle,
146 door: DoorState::Closed,
147 max_speed: params.max_speed,
148 acceleration: params.acceleration,
149 deceleration: params.deceleration,
150 weight_capacity: params.weight_capacity,
151 current_load: 0.0,
152 riders: Vec::new(),
153 target_stop: None,
154 door_transition_ticks: params.door_transition_ticks,
155 door_open_ticks: params.door_open_ticks,
156 line,
157 repositioning: false,
158 restricted_stops: params.restricted_stops.clone(),
159 inspection_speed_factor: params.inspection_speed_factor,
160 going_up: true,
161 going_down: true,
162 move_count: 0,
163 door_command_queue: Vec::new(),
164 manual_target_velocity: None,
165 },
166 );
167 self.world
168 .set_destination_queue(eid, crate::components::DestinationQueue::new());
169 self.groups[group_idx].lines_mut()[line_idx]
170 .elevators_mut()
171 .push(eid);
172 self.groups[group_idx].push_elevator(eid);
173
174 let line_name = self.world.line(line).map(|l| l.name.clone());
176 if let Some(name) = line_name
177 && let Some(tags) = self
178 .world
179 .resource_mut::<crate::tagged_metrics::MetricTags>()
180 {
181 tags.tag(eid, format!("line:{name}"));
182 }
183
184 self.mark_topo_dirty();
185 self.events.emit(Event::ElevatorAdded {
186 elevator: eid,
187 line,
188 group: group_id,
189 tick: self.tick,
190 });
191 Ok(eid)
192 }
193
194 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
202 let group_id = params.group;
203 let group = self
204 .groups
205 .iter_mut()
206 .find(|g| g.id() == group_id)
207 .ok_or(SimError::GroupNotFound(group_id))?;
208
209 let line_tag = format!("line:{}", params.name);
210
211 let eid = self.world.spawn();
212 self.world.set_line(
213 eid,
214 Line {
215 name: params.name.clone(),
216 group: group_id,
217 orientation: params.orientation,
218 position: params.position,
219 min_position: params.min_position,
220 max_position: params.max_position,
221 max_cars: params.max_cars,
222 },
223 );
224
225 group
226 .lines_mut()
227 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
228
229 if let Some(tags) = self
231 .world
232 .resource_mut::<crate::tagged_metrics::MetricTags>()
233 {
234 tags.tag(eid, line_tag);
235 }
236
237 self.mark_topo_dirty();
238 self.events.emit(Event::LineAdded {
239 line: eid,
240 group: group_id,
241 tick: self.tick,
242 });
243 Ok(eid)
244 }
245
246 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
256 let (group_idx, line_idx) = self.find_line(line)?;
257
258 let group_id = self.groups[group_idx].id();
259
260 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
262 .elevators()
263 .to_vec();
264
265 for eid in &elevator_ids {
267 let _ = self.disable(*eid);
269 }
270
271 self.groups[group_idx].lines_mut().remove(line_idx);
273
274 self.groups[group_idx].rebuild_caches();
276
277 self.world.remove_line(line);
279
280 self.mark_topo_dirty();
281 self.events.emit(Event::LineRemoved {
282 line,
283 group: group_id,
284 tick: self.tick,
285 });
286 Ok(())
287 }
288
289 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
298 let line = self
299 .world
300 .elevator(elevator)
301 .ok_or(SimError::EntityNotFound(elevator))?
302 .line();
303
304 let _ = self.disable(elevator);
306
307 let mut group_id = GroupId(0);
309 if let Ok((group_idx, line_idx)) = self.find_line(line) {
310 self.groups[group_idx].lines_mut()[line_idx]
311 .elevators_mut()
312 .retain(|&e| e != elevator);
313 self.groups[group_idx].rebuild_caches();
314
315 group_id = self.groups[group_idx].id();
317 if let Some(dispatcher) = self.dispatchers.get_mut(&group_id) {
318 dispatcher.notify_removed(elevator);
319 }
320 }
321
322 self.events.emit(Event::ElevatorRemoved {
323 elevator,
324 line,
325 group: group_id,
326 tick: self.tick,
327 });
328
329 self.world.despawn(elevator);
331
332 self.mark_topo_dirty();
333 Ok(())
334 }
335
336 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
345 if self.world.stop(stop).is_none() {
346 return Err(SimError::EntityNotFound(stop));
347 }
348
349 let _ = self.disable(stop);
351
352 let elevator_ids: Vec<EntityId> =
356 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
357 for eid in elevator_ids {
358 if let Some(car) = self.world.elevator_mut(eid) {
359 if car.target_stop == Some(stop) {
360 car.target_stop = None;
361 }
362 car.restricted_stops.remove(&stop);
363 }
364 if let Some(q) = self.world.destination_queue_mut(eid) {
365 q.retain(|s| s != stop);
366 }
367 }
368
369 for group in &mut self.groups {
371 for line_info in group.lines_mut() {
372 line_info.serves_mut().retain(|&s| s != stop);
373 }
374 group.rebuild_caches();
375 }
376
377 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
379 sorted.0.retain(|&(_, s)| s != stop);
380 }
381
382 self.stop_lookup.retain(|_, &mut eid| eid != stop);
384
385 self.events.emit(Event::StopRemoved {
386 stop,
387 tick: self.tick,
388 });
389
390 self.world.despawn(stop);
392
393 self.mark_topo_dirty();
394 Ok(())
395 }
396
397 pub fn add_group(
399 &mut self,
400 name: impl Into<String>,
401 dispatch: impl DispatchStrategy + 'static,
402 ) -> GroupId {
403 let next_id = self
404 .groups
405 .iter()
406 .map(|g| g.id().0)
407 .max()
408 .map_or(0, |m| m + 1);
409 let group_id = GroupId(next_id);
410
411 self.groups
412 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
413
414 self.dispatchers.insert(group_id, Box::new(dispatch));
415 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
416 self.mark_topo_dirty();
417 group_id
418 }
419
420 pub fn assign_line_to_group(
427 &mut self,
428 line: EntityId,
429 new_group: GroupId,
430 ) -> Result<GroupId, SimError> {
431 let (old_group_idx, line_idx) = self.find_line(line)?;
432
433 if !self.groups.iter().any(|g| g.id() == new_group) {
435 return Err(SimError::GroupNotFound(new_group));
436 }
437
438 let old_group_id = self.groups[old_group_idx].id();
439
440 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
442 self.groups[old_group_idx].rebuild_caches();
443
444 let new_group_idx = self
449 .groups
450 .iter()
451 .position(|g| g.id() == new_group)
452 .ok_or(SimError::GroupNotFound(new_group))?;
453 self.groups[new_group_idx].lines_mut().push(line_info);
454 self.groups[new_group_idx].rebuild_caches();
455
456 if let Some(line_comp) = self.world.line_mut(line) {
458 line_comp.group = new_group;
459 }
460
461 self.mark_topo_dirty();
462 self.events.emit(Event::LineReassigned {
463 line,
464 old_group: old_group_id,
465 new_group,
466 tick: self.tick,
467 });
468
469 Ok(old_group_id)
470 }
471
472 pub fn reassign_elevator_to_line(
483 &mut self,
484 elevator: EntityId,
485 new_line: EntityId,
486 ) -> Result<(), SimError> {
487 let old_line = self
488 .world
489 .elevator(elevator)
490 .ok_or(SimError::EntityNotFound(elevator))?
491 .line();
492
493 if old_line == new_line {
494 return Ok(());
495 }
496
497 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
499 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
500
501 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
503 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
504 .elevators()
505 .len();
506 if current_count >= max {
507 return Err(SimError::InvalidConfig {
508 field: "line.max_cars",
509 reason: format!("target line already has {current_count} cars (max {max})"),
510 });
511 }
512 }
513
514 let old_group_id = self.groups[old_group_idx].id();
515 let new_group_id = self.groups[new_group_idx].id();
516
517 self.groups[old_group_idx].lines_mut()[old_line_idx]
518 .elevators_mut()
519 .retain(|&e| e != elevator);
520 self.groups[new_group_idx].lines_mut()[new_line_idx]
521 .elevators_mut()
522 .push(elevator);
523
524 if let Some(car) = self.world.elevator_mut(elevator) {
525 car.line = new_line;
526 }
527
528 self.groups[old_group_idx].rebuild_caches();
529 if new_group_idx != old_group_idx {
530 self.groups[new_group_idx].rebuild_caches();
531
532 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
536 old_dispatcher.notify_removed(elevator);
537 }
538 }
539
540 self.mark_topo_dirty();
541
542 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
544 elevator,
545 old_line,
546 new_line,
547 tick: self.tick,
548 });
549
550 Ok(())
551 }
552
553 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
560 if self.world.stop(stop).is_none() {
562 return Err(SimError::EntityNotFound(stop));
563 }
564
565 let (group_idx, line_idx) = self.find_line(line)?;
566
567 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
568 if !li.serves().contains(&stop) {
569 li.serves_mut().push(stop);
570 }
571
572 self.groups[group_idx].push_stop(stop);
573
574 self.mark_topo_dirty();
575 Ok(())
576 }
577
578 pub fn remove_stop_from_line(
584 &mut self,
585 stop: EntityId,
586 line: EntityId,
587 ) -> Result<(), SimError> {
588 let (group_idx, line_idx) = self.find_line(line)?;
589
590 self.groups[group_idx].lines_mut()[line_idx]
591 .serves_mut()
592 .retain(|&s| s != stop);
593
594 self.groups[group_idx].rebuild_caches();
596
597 self.mark_topo_dirty();
598 Ok(())
599 }
600
601 #[must_use]
605 pub fn all_lines(&self) -> Vec<EntityId> {
606 self.groups
607 .iter()
608 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
609 .collect()
610 }
611
612 #[must_use]
614 pub fn line_count(&self) -> usize {
615 self.groups.iter().map(|g| g.lines().len()).sum()
616 }
617
618 #[must_use]
620 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
621 self.groups
622 .iter()
623 .find(|g| g.id() == group)
624 .map_or_else(Vec::new, |g| {
625 g.lines().iter().map(LineInfo::entity).collect()
626 })
627 }
628
629 #[must_use]
631 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
632 self.groups
633 .iter()
634 .flat_map(ElevatorGroup::lines)
635 .find(|li| li.entity() == line)
636 .map_or_else(Vec::new, |li| li.elevators().to_vec())
637 }
638
639 #[must_use]
641 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
642 self.groups
643 .iter()
644 .flat_map(ElevatorGroup::lines)
645 .find(|li| li.entity() == line)
646 .map_or_else(Vec::new, |li| li.serves().to_vec())
647 }
648
649 #[must_use]
651 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
652 self.groups
653 .iter()
654 .flat_map(ElevatorGroup::lines)
655 .find(|li| li.elevators().contains(&elevator))
656 .map(LineInfo::entity)
657 }
658
659 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
661 self.world
662 .iter_elevators()
663 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
664 }
665
666 #[must_use]
668 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
669 self.groups
670 .iter()
671 .flat_map(ElevatorGroup::lines)
672 .filter(|li| li.serves().contains(&stop))
673 .map(LineInfo::entity)
674 .collect()
675 }
676
677 #[must_use]
679 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
680 self.groups
681 .iter()
682 .filter(|g| g.stop_entities().contains(&stop))
683 .map(ElevatorGroup::id)
684 .collect()
685 }
686
687 pub(super) fn ensure_graph_built(&self) {
691 if let Ok(mut graph) = self.topo_graph.lock()
692 && graph.is_dirty()
693 {
694 graph.rebuild(&self.groups);
695 }
696 }
697
698 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
700 self.ensure_graph_built();
701 self.topo_graph
702 .lock()
703 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
704 }
705
706 pub fn transfer_points(&self) -> Vec<EntityId> {
708 self.ensure_graph_built();
709 TopologyGraph::transfer_points(&self.groups)
710 }
711
712 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
714 self.ensure_graph_built();
715 self.topo_graph
716 .lock()
717 .ok()
718 .and_then(|g| g.shortest_route(from, to))
719 }
720}