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 fn mark_topo_dirty(&self) {
24 if let Ok(mut g) = self.topo_graph.lock() {
25 g.mark_dirty();
26 }
27 }
28
29 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 super::construction::validate_elevator_physics(
118 params.max_speed.value(),
119 params.acceleration.value(),
120 params.deceleration.value(),
121 params.weight_capacity.value(),
122 params.inspection_speed_factor,
123 params.door_transition_ticks,
124 params.door_open_ticks,
125 )?;
126 if !starting_position.is_finite() {
127 return Err(SimError::InvalidConfig {
128 field: "starting_position",
129 reason: format!(
130 "must be finite (got {starting_position}); NaN/±inf corrupt \
131 SortedStops ordering and find_stop_at_position lookup"
132 ),
133 });
134 }
135
136 let group_id = self
137 .world
138 .line(line)
139 .map(|l| l.group)
140 .ok_or(SimError::LineNotFound(line))?;
141
142 let (group_idx, line_idx) = self.find_line(line)?;
143
144 if let Some(max) = self.world.line(line).and_then(Line::max_cars) {
146 let current_count = self.groups[group_idx].lines()[line_idx].elevators().len();
147 if current_count >= max {
148 return Err(SimError::InvalidConfig {
149 field: "line.max_cars",
150 reason: format!("line already has {current_count} cars (max {max})"),
151 });
152 }
153 }
154
155 let eid = self.world.spawn();
156 self.world.set_position(
157 eid,
158 Position {
159 value: starting_position,
160 },
161 );
162 self.world.set_velocity(eid, Velocity { value: 0.0 });
163 self.world.set_elevator(
164 eid,
165 Elevator {
166 phase: ElevatorPhase::Idle,
167 door: DoorState::Closed,
168 max_speed: params.max_speed,
169 acceleration: params.acceleration,
170 deceleration: params.deceleration,
171 weight_capacity: params.weight_capacity,
172 current_load: crate::components::Weight::ZERO,
173 riders: Vec::new(),
174 target_stop: None,
175 door_transition_ticks: params.door_transition_ticks,
176 door_open_ticks: params.door_open_ticks,
177 line,
178 repositioning: false,
179 restricted_stops: params.restricted_stops.clone(),
180 inspection_speed_factor: params.inspection_speed_factor,
181 going_up: true,
182 going_down: true,
183 move_count: 0,
184 door_command_queue: Vec::new(),
185 manual_target_velocity: None,
186 },
187 );
188 self.world
189 .set_destination_queue(eid, crate::components::DestinationQueue::new());
190 self.groups[group_idx].lines_mut()[line_idx]
191 .elevators_mut()
192 .push(eid);
193 self.groups[group_idx].push_elevator(eid);
194
195 let line_name = self.world.line(line).map(|l| l.name.clone());
197 if let Some(name) = line_name
198 && let Some(tags) = self
199 .world
200 .resource_mut::<crate::tagged_metrics::MetricTags>()
201 {
202 tags.tag(eid, format!("line:{name}"));
203 }
204
205 self.mark_topo_dirty();
206 self.events.emit(Event::ElevatorAdded {
207 elevator: eid,
208 line,
209 group: group_id,
210 tick: self.tick,
211 });
212 Ok(eid)
213 }
214
215 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
223 let group_id = params.group;
224 let group = self
225 .groups
226 .iter_mut()
227 .find(|g| g.id() == group_id)
228 .ok_or(SimError::GroupNotFound(group_id))?;
229
230 let line_tag = format!("line:{}", params.name);
231
232 let eid = self.world.spawn();
233 self.world.set_line(
234 eid,
235 Line {
236 name: params.name.clone(),
237 group: group_id,
238 orientation: params.orientation,
239 position: params.position,
240 min_position: params.min_position,
241 max_position: params.max_position,
242 max_cars: params.max_cars,
243 },
244 );
245
246 group
247 .lines_mut()
248 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
249
250 if let Some(tags) = self
252 .world
253 .resource_mut::<crate::tagged_metrics::MetricTags>()
254 {
255 tags.tag(eid, line_tag);
256 }
257
258 self.mark_topo_dirty();
259 self.events.emit(Event::LineAdded {
260 line: eid,
261 group: group_id,
262 tick: self.tick,
263 });
264 Ok(eid)
265 }
266
267 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
277 let (group_idx, line_idx) = self.find_line(line)?;
278
279 let group_id = self.groups[group_idx].id();
280
281 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
283 .elevators()
284 .to_vec();
285
286 for eid in &elevator_ids {
288 let _ = self.disable(*eid);
290 }
291
292 self.groups[group_idx].lines_mut().remove(line_idx);
294
295 self.groups[group_idx].rebuild_caches();
297
298 self.world.remove_line(line);
300
301 self.mark_topo_dirty();
302 self.events.emit(Event::LineRemoved {
303 line,
304 group: group_id,
305 tick: self.tick,
306 });
307 Ok(())
308 }
309
310 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
319 let line = self
320 .world
321 .elevator(elevator)
322 .ok_or(SimError::EntityNotFound(elevator))?
323 .line();
324
325 let _ = self.disable(elevator);
327
328 let resolved_group: Option<GroupId> = match self.find_line(line) {
332 Ok((group_idx, line_idx)) => {
333 self.groups[group_idx].lines_mut()[line_idx]
334 .elevators_mut()
335 .retain(|&e| e != elevator);
336 self.groups[group_idx].rebuild_caches();
337
338 let gid = self.groups[group_idx].id();
339 if let Some(dispatcher) = self.dispatchers.get_mut(&gid) {
341 dispatcher.notify_removed(elevator);
342 }
343 Some(gid)
344 }
345 Err(_) => None,
346 };
347
348 if let Some(group_id) = resolved_group {
352 self.events.emit(Event::ElevatorRemoved {
353 elevator,
354 line,
355 group: group_id,
356 tick: self.tick,
357 });
358 }
359
360 self.world.despawn(elevator);
362
363 self.mark_topo_dirty();
364 Ok(())
365 }
366
367 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
376 if self.world.stop(stop).is_none() {
377 return Err(SimError::EntityNotFound(stop));
378 }
379
380 let residents: Vec<EntityId> = self
383 .rider_index
384 .residents_at(stop)
385 .iter()
386 .copied()
387 .collect();
388 if !residents.is_empty() {
389 self.events
390 .emit(Event::ResidentsAtRemovedStop { stop, residents });
391 }
392
393 let _ = self.disable(stop);
395
396 let elevator_ids: Vec<EntityId> =
400 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
401 for eid in elevator_ids {
402 if let Some(car) = self.world.elevator_mut(eid) {
403 if car.target_stop == Some(stop) {
404 car.target_stop = None;
405 }
406 car.restricted_stops.remove(&stop);
407 }
408 if let Some(q) = self.world.destination_queue_mut(eid) {
409 q.retain(|s| s != stop);
410 }
411 if let Some(calls) = self.world.car_calls_mut(eid) {
416 calls.retain(|c| c.floor != stop);
417 }
418 }
419
420 for group in &mut self.groups {
422 for line_info in group.lines_mut() {
423 line_info.serves_mut().retain(|&s| s != stop);
424 }
425 group.rebuild_caches();
426 }
427
428 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
430 sorted.0.retain(|&(_, s)| s != stop);
431 }
432
433 self.stop_lookup.retain(|_, &mut eid| eid != stop);
435
436 self.events.emit(Event::StopRemoved {
437 stop,
438 tick: self.tick,
439 });
440
441 self.world.despawn(stop);
443
444 self.mark_topo_dirty();
445 Ok(())
446 }
447
448 pub fn add_group(
450 &mut self,
451 name: impl Into<String>,
452 dispatch: impl DispatchStrategy + 'static,
453 ) -> GroupId {
454 let next_id = self
455 .groups
456 .iter()
457 .map(|g| g.id().0)
458 .max()
459 .map_or(0, |m| m + 1);
460 let group_id = GroupId(next_id);
461
462 self.groups
463 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
464
465 self.dispatchers.insert(group_id, Box::new(dispatch));
466 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
467 self.mark_topo_dirty();
468 group_id
469 }
470
471 pub fn assign_line_to_group(
478 &mut self,
479 line: EntityId,
480 new_group: GroupId,
481 ) -> Result<GroupId, SimError> {
482 let (old_group_idx, line_idx) = self.find_line(line)?;
483
484 if !self.groups.iter().any(|g| g.id() == new_group) {
486 return Err(SimError::GroupNotFound(new_group));
487 }
488
489 let old_group_id = self.groups[old_group_idx].id();
490
491 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
493 self.groups[old_group_idx].rebuild_caches();
494
495 let new_group_idx = self
500 .groups
501 .iter()
502 .position(|g| g.id() == new_group)
503 .ok_or(SimError::GroupNotFound(new_group))?;
504 self.groups[new_group_idx].lines_mut().push(line_info);
505 self.groups[new_group_idx].rebuild_caches();
506
507 if let Some(line_comp) = self.world.line_mut(line) {
509 line_comp.group = new_group;
510 }
511
512 self.mark_topo_dirty();
513 self.events.emit(Event::LineReassigned {
514 line,
515 old_group: old_group_id,
516 new_group,
517 tick: self.tick,
518 });
519
520 Ok(old_group_id)
521 }
522
523 pub fn reassign_elevator_to_line(
534 &mut self,
535 elevator: EntityId,
536 new_line: EntityId,
537 ) -> Result<(), SimError> {
538 let old_line = self
539 .world
540 .elevator(elevator)
541 .ok_or(SimError::EntityNotFound(elevator))?
542 .line();
543
544 if old_line == new_line {
545 return Ok(());
546 }
547
548 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
550 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
551
552 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
554 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
555 .elevators()
556 .len();
557 if current_count >= max {
558 return Err(SimError::InvalidConfig {
559 field: "line.max_cars",
560 reason: format!("target line already has {current_count} cars (max {max})"),
561 });
562 }
563 }
564
565 let old_group_id = self.groups[old_group_idx].id();
566 let new_group_id = self.groups[new_group_idx].id();
567
568 self.groups[old_group_idx].lines_mut()[old_line_idx]
569 .elevators_mut()
570 .retain(|&e| e != elevator);
571 self.groups[new_group_idx].lines_mut()[new_line_idx]
572 .elevators_mut()
573 .push(elevator);
574
575 if let Some(car) = self.world.elevator_mut(elevator) {
576 car.line = new_line;
577 }
578
579 self.groups[old_group_idx].rebuild_caches();
580 if new_group_idx != old_group_idx {
581 self.groups[new_group_idx].rebuild_caches();
582
583 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
587 old_dispatcher.notify_removed(elevator);
588 }
589 }
590
591 self.mark_topo_dirty();
592
593 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
595 elevator,
596 old_line,
597 new_line,
598 tick: self.tick,
599 });
600
601 Ok(())
602 }
603
604 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
611 if self.world.stop(stop).is_none() {
613 return Err(SimError::EntityNotFound(stop));
614 }
615
616 let (group_idx, line_idx) = self.find_line(line)?;
617
618 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
619 if !li.serves().contains(&stop) {
620 li.serves_mut().push(stop);
621 }
622
623 self.groups[group_idx].push_stop(stop);
624
625 self.mark_topo_dirty();
626 Ok(())
627 }
628
629 pub fn remove_stop_from_line(
635 &mut self,
636 stop: EntityId,
637 line: EntityId,
638 ) -> Result<(), SimError> {
639 let (group_idx, line_idx) = self.find_line(line)?;
640
641 self.groups[group_idx].lines_mut()[line_idx]
642 .serves_mut()
643 .retain(|&s| s != stop);
644
645 self.groups[group_idx].rebuild_caches();
647
648 self.mark_topo_dirty();
649 Ok(())
650 }
651
652 #[must_use]
656 pub fn all_lines(&self) -> Vec<EntityId> {
657 self.groups
658 .iter()
659 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
660 .collect()
661 }
662
663 #[must_use]
665 pub fn line_count(&self) -> usize {
666 self.groups.iter().map(|g| g.lines().len()).sum()
667 }
668
669 #[must_use]
671 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
672 self.groups
673 .iter()
674 .find(|g| g.id() == group)
675 .map_or_else(Vec::new, |g| {
676 g.lines().iter().map(LineInfo::entity).collect()
677 })
678 }
679
680 #[must_use]
682 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
683 self.groups
684 .iter()
685 .flat_map(ElevatorGroup::lines)
686 .find(|li| li.entity() == line)
687 .map_or_else(Vec::new, |li| li.elevators().to_vec())
688 }
689
690 #[must_use]
692 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
693 self.groups
694 .iter()
695 .flat_map(ElevatorGroup::lines)
696 .find(|li| li.entity() == line)
697 .map_or_else(Vec::new, |li| li.serves().to_vec())
698 }
699
700 #[must_use]
702 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
703 self.groups
704 .iter()
705 .flat_map(ElevatorGroup::lines)
706 .find(|li| li.elevators().contains(&elevator))
707 .map(LineInfo::entity)
708 }
709
710 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
712 self.world
713 .iter_elevators()
714 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
715 }
716
717 #[must_use]
719 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
720 self.groups
721 .iter()
722 .flat_map(ElevatorGroup::lines)
723 .filter(|li| li.serves().contains(&stop))
724 .map(LineInfo::entity)
725 .collect()
726 }
727
728 #[must_use]
730 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
731 self.groups
732 .iter()
733 .filter(|g| g.stop_entities().contains(&stop))
734 .map(ElevatorGroup::id)
735 .collect()
736 }
737
738 fn ensure_graph_built(&self) {
742 if let Ok(mut graph) = self.topo_graph.lock()
743 && graph.is_dirty()
744 {
745 graph.rebuild(&self.groups);
746 }
747 }
748
749 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
751 self.ensure_graph_built();
752 self.topo_graph
753 .lock()
754 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
755 }
756
757 pub fn transfer_points(&self) -> Vec<EntityId> {
759 self.ensure_graph_built();
760 TopologyGraph::transfer_points(&self.groups)
761 }
762
763 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
765 self.ensure_graph_built();
766 self.topo_graph
767 .lock()
768 .ok()
769 .and_then(|g| g.shortest_route(from, to))
770 }
771}