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 params.bypass_load_up_pct,
126 params.bypass_load_down_pct,
127 )?;
128 if !starting_position.is_finite() {
129 return Err(SimError::InvalidConfig {
130 field: "starting_position",
131 reason: format!(
132 "must be finite (got {starting_position}); NaN/±inf corrupt \
133 SortedStops ordering and find_stop_at_position lookup"
134 ),
135 });
136 }
137
138 let group_id = self
139 .world
140 .line(line)
141 .map(|l| l.group)
142 .ok_or(SimError::LineNotFound(line))?;
143
144 let (group_idx, line_idx) = self.find_line(line)?;
145
146 if let Some(max) = self.world.line(line).and_then(Line::max_cars) {
148 let current_count = self.groups[group_idx].lines()[line_idx].elevators().len();
149 if current_count >= max {
150 return Err(SimError::InvalidConfig {
151 field: "line.max_cars",
152 reason: format!("line already has {current_count} cars (max {max})"),
153 });
154 }
155 }
156
157 let eid = self.world.spawn();
158 self.world.set_position(
159 eid,
160 Position {
161 value: starting_position,
162 },
163 );
164 self.world.set_velocity(eid, Velocity { value: 0.0 });
165 self.world.set_elevator(
166 eid,
167 Elevator {
168 phase: ElevatorPhase::Idle,
169 door: DoorState::Closed,
170 max_speed: params.max_speed,
171 acceleration: params.acceleration,
172 deceleration: params.deceleration,
173 weight_capacity: params.weight_capacity,
174 current_load: crate::components::Weight::ZERO,
175 riders: Vec::new(),
176 target_stop: None,
177 door_transition_ticks: params.door_transition_ticks,
178 door_open_ticks: params.door_open_ticks,
179 line,
180 repositioning: false,
181 restricted_stops: params.restricted_stops.clone(),
182 inspection_speed_factor: params.inspection_speed_factor,
183 going_up: true,
184 going_down: true,
185 move_count: 0,
186 door_command_queue: Vec::new(),
187 manual_target_velocity: None,
188 bypass_load_up_pct: params.bypass_load_up_pct,
189 bypass_load_down_pct: params.bypass_load_down_pct,
190 },
191 );
192 self.world
193 .set_destination_queue(eid, crate::components::DestinationQueue::new());
194 self.groups[group_idx].lines_mut()[line_idx]
195 .elevators_mut()
196 .push(eid);
197 self.groups[group_idx].push_elevator(eid);
198
199 let line_name = self.world.line(line).map(|l| l.name.clone());
201 if let Some(name) = line_name
202 && let Some(tags) = self
203 .world
204 .resource_mut::<crate::tagged_metrics::MetricTags>()
205 {
206 tags.tag(eid, format!("line:{name}"));
207 }
208
209 self.mark_topo_dirty();
210 self.events.emit(Event::ElevatorAdded {
211 elevator: eid,
212 line,
213 group: group_id,
214 tick: self.tick,
215 });
216 Ok(eid)
217 }
218
219 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
227 let group_id = params.group;
228 let group = self
229 .groups
230 .iter_mut()
231 .find(|g| g.id() == group_id)
232 .ok_or(SimError::GroupNotFound(group_id))?;
233
234 let line_tag = format!("line:{}", params.name);
235
236 let eid = self.world.spawn();
237 self.world.set_line(
238 eid,
239 Line {
240 name: params.name.clone(),
241 group: group_id,
242 orientation: params.orientation,
243 position: params.position,
244 min_position: params.min_position,
245 max_position: params.max_position,
246 max_cars: params.max_cars,
247 },
248 );
249
250 group
251 .lines_mut()
252 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
253
254 if let Some(tags) = self
256 .world
257 .resource_mut::<crate::tagged_metrics::MetricTags>()
258 {
259 tags.tag(eid, line_tag);
260 }
261
262 self.mark_topo_dirty();
263 self.events.emit(Event::LineAdded {
264 line: eid,
265 group: group_id,
266 tick: self.tick,
267 });
268 Ok(eid)
269 }
270
271 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
281 let (group_idx, line_idx) = self.find_line(line)?;
282
283 let group_id = self.groups[group_idx].id();
284
285 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
287 .elevators()
288 .to_vec();
289
290 for eid in &elevator_ids {
292 let _ = self.disable(*eid);
294 }
295
296 self.groups[group_idx].lines_mut().remove(line_idx);
298
299 self.groups[group_idx].rebuild_caches();
301
302 self.world.remove_line(line);
304
305 self.mark_topo_dirty();
306 self.events.emit(Event::LineRemoved {
307 line,
308 group: group_id,
309 tick: self.tick,
310 });
311 Ok(())
312 }
313
314 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
323 let line = self
324 .world
325 .elevator(elevator)
326 .ok_or(SimError::EntityNotFound(elevator))?
327 .line();
328
329 let _ = self.disable(elevator);
331
332 let resolved_group: Option<GroupId> = match self.find_line(line) {
343 Ok((group_idx, line_idx)) => {
344 self.groups[group_idx].lines_mut()[line_idx]
345 .elevators_mut()
346 .retain(|&e| e != elevator);
347 self.groups[group_idx].rebuild_caches();
348 Some(self.groups[group_idx].id())
349 }
350 Err(_) => None,
351 };
352
353 if let Some(group_id) = resolved_group {
357 self.events.emit(Event::ElevatorRemoved {
358 elevator,
359 line,
360 group: group_id,
361 tick: self.tick,
362 });
363 }
364
365 self.world.despawn(elevator);
367
368 self.mark_topo_dirty();
369 Ok(())
370 }
371
372 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
381 if self.world.stop(stop).is_none() {
382 return Err(SimError::EntityNotFound(stop));
383 }
384
385 let residents: Vec<EntityId> = self
388 .rider_index
389 .residents_at(stop)
390 .iter()
391 .copied()
392 .collect();
393 if !residents.is_empty() {
394 self.events
395 .emit(Event::ResidentsAtRemovedStop { stop, residents });
396 }
397
398 self.disable_stop_inner(stop, true);
402 self.world.disable(stop);
403 self.events.emit(Event::EntityDisabled {
404 entity: stop,
405 tick: self.tick,
406 });
407
408 let elevator_ids: Vec<EntityId> =
412 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
413 for eid in elevator_ids {
414 if let Some(car) = self.world.elevator_mut(eid) {
415 if car.target_stop == Some(stop) {
416 car.target_stop = None;
417 }
418 car.restricted_stops.remove(&stop);
419 }
420 if let Some(q) = self.world.destination_queue_mut(eid) {
421 q.retain(|s| s != stop);
422 }
423 if let Some(calls) = self.world.car_calls_mut(eid) {
428 calls.retain(|c| c.floor != stop);
429 }
430 }
431
432 for group in &mut self.groups {
434 for line_info in group.lines_mut() {
435 line_info.serves_mut().retain(|&s| s != stop);
436 }
437 group.rebuild_caches();
438 }
439
440 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
442 sorted.0.retain(|&(_, s)| s != stop);
443 }
444
445 self.stop_lookup.retain(|_, &mut eid| eid != stop);
447
448 self.events.emit(Event::StopRemoved {
449 stop,
450 tick: self.tick,
451 });
452
453 self.world.despawn(stop);
455
456 self.rider_index.rebuild(&self.world);
460
461 self.mark_topo_dirty();
462 Ok(())
463 }
464
465 pub fn add_group(
467 &mut self,
468 name: impl Into<String>,
469 dispatch: impl DispatchStrategy + 'static,
470 ) -> GroupId {
471 let next_id = self
472 .groups
473 .iter()
474 .map(|g| g.id().0)
475 .max()
476 .map_or(0, |m| m + 1);
477 let group_id = GroupId(next_id);
478
479 self.groups
480 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
481
482 self.dispatchers.insert(group_id, Box::new(dispatch));
483 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
484 self.mark_topo_dirty();
485 group_id
486 }
487
488 pub fn assign_line_to_group(
495 &mut self,
496 line: EntityId,
497 new_group: GroupId,
498 ) -> Result<GroupId, SimError> {
499 let (old_group_idx, line_idx) = self.find_line(line)?;
500
501 if !self.groups.iter().any(|g| g.id() == new_group) {
503 return Err(SimError::GroupNotFound(new_group));
504 }
505
506 let old_group_id = self.groups[old_group_idx].id();
507
508 if old_group_id == new_group {
513 return Ok(old_group_id);
514 }
515
516 let elevators_to_notify: Vec<EntityId> = self.groups[old_group_idx].lines()[line_idx]
521 .elevators()
522 .to_vec();
523 if let Some(dispatcher) = self.dispatchers.get_mut(&old_group_id) {
524 for eid in &elevators_to_notify {
525 dispatcher.notify_removed(*eid);
526 }
527 }
528
529 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
531 self.groups[old_group_idx].rebuild_caches();
532
533 let new_group_idx = self
538 .groups
539 .iter()
540 .position(|g| g.id() == new_group)
541 .ok_or(SimError::GroupNotFound(new_group))?;
542 self.groups[new_group_idx].lines_mut().push(line_info);
543 self.groups[new_group_idx].rebuild_caches();
544
545 if let Some(line_comp) = self.world.line_mut(line) {
547 line_comp.group = new_group;
548 }
549
550 self.mark_topo_dirty();
551 self.events.emit(Event::LineReassigned {
552 line,
553 old_group: old_group_id,
554 new_group,
555 tick: self.tick,
556 });
557
558 Ok(old_group_id)
559 }
560
561 pub fn reassign_elevator_to_line(
572 &mut self,
573 elevator: EntityId,
574 new_line: EntityId,
575 ) -> Result<(), SimError> {
576 let old_line = self
577 .world
578 .elevator(elevator)
579 .ok_or(SimError::EntityNotFound(elevator))?
580 .line();
581
582 if old_line == new_line {
583 return Ok(());
584 }
585
586 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
588 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
589
590 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
592 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
593 .elevators()
594 .len();
595 if current_count >= max {
596 return Err(SimError::InvalidConfig {
597 field: "line.max_cars",
598 reason: format!("target line already has {current_count} cars (max {max})"),
599 });
600 }
601 }
602
603 let old_group_id = self.groups[old_group_idx].id();
604 let new_group_id = self.groups[new_group_idx].id();
605
606 self.groups[old_group_idx].lines_mut()[old_line_idx]
607 .elevators_mut()
608 .retain(|&e| e != elevator);
609 self.groups[new_group_idx].lines_mut()[new_line_idx]
610 .elevators_mut()
611 .push(elevator);
612
613 if let Some(car) = self.world.elevator_mut(elevator) {
614 car.line = new_line;
615 }
616
617 self.groups[old_group_idx].rebuild_caches();
618 if new_group_idx != old_group_idx {
619 self.groups[new_group_idx].rebuild_caches();
620
621 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
625 old_dispatcher.notify_removed(elevator);
626 }
627 }
628
629 self.mark_topo_dirty();
630
631 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
633 elevator,
634 old_line,
635 new_line,
636 tick: self.tick,
637 });
638
639 Ok(())
640 }
641
642 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
649 if self.world.stop(stop).is_none() {
651 return Err(SimError::EntityNotFound(stop));
652 }
653
654 let (group_idx, line_idx) = self.find_line(line)?;
655
656 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
657 if !li.serves().contains(&stop) {
658 li.serves_mut().push(stop);
659 }
660
661 self.groups[group_idx].push_stop(stop);
662
663 self.mark_topo_dirty();
664 Ok(())
665 }
666
667 pub fn remove_stop_from_line(
673 &mut self,
674 stop: EntityId,
675 line: EntityId,
676 ) -> Result<(), SimError> {
677 let (group_idx, line_idx) = self.find_line(line)?;
678
679 self.groups[group_idx].lines_mut()[line_idx]
680 .serves_mut()
681 .retain(|&s| s != stop);
682
683 self.groups[group_idx].rebuild_caches();
685
686 self.mark_topo_dirty();
687 Ok(())
688 }
689
690 #[must_use]
694 pub fn all_lines(&self) -> Vec<EntityId> {
695 self.groups
696 .iter()
697 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
698 .collect()
699 }
700
701 #[must_use]
703 pub fn line_count(&self) -> usize {
704 self.groups.iter().map(|g| g.lines().len()).sum()
705 }
706
707 #[must_use]
709 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
710 self.groups
711 .iter()
712 .find(|g| g.id() == group)
713 .map_or_else(Vec::new, |g| {
714 g.lines().iter().map(LineInfo::entity).collect()
715 })
716 }
717
718 #[must_use]
720 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
721 self.groups
722 .iter()
723 .flat_map(ElevatorGroup::lines)
724 .find(|li| li.entity() == line)
725 .map_or_else(Vec::new, |li| li.elevators().to_vec())
726 }
727
728 #[must_use]
730 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
731 self.groups
732 .iter()
733 .flat_map(ElevatorGroup::lines)
734 .find(|li| li.entity() == line)
735 .map_or_else(Vec::new, |li| li.serves().to_vec())
736 }
737
738 #[must_use]
740 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
741 self.groups
742 .iter()
743 .flat_map(ElevatorGroup::lines)
744 .find(|li| li.elevators().contains(&elevator))
745 .map(LineInfo::entity)
746 }
747
748 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
750 self.world
751 .iter_elevators()
752 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
753 }
754
755 #[must_use]
757 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
758 self.groups
759 .iter()
760 .flat_map(ElevatorGroup::lines)
761 .filter(|li| li.serves().contains(&stop))
762 .map(LineInfo::entity)
763 .collect()
764 }
765
766 #[must_use]
768 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
769 self.groups
770 .iter()
771 .filter(|g| g.stop_entities().contains(&stop))
772 .map(ElevatorGroup::id)
773 .collect()
774 }
775
776 fn ensure_graph_built(&self) {
780 if let Ok(mut graph) = self.topo_graph.lock()
781 && graph.is_dirty()
782 {
783 graph.rebuild(&self.groups);
784 }
785 }
786
787 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
789 self.ensure_graph_built();
790 self.topo_graph
791 .lock()
792 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
793 }
794
795 pub fn transfer_points(&self) -> Vec<EntityId> {
797 self.ensure_graph_built();
798 TopologyGraph::transfer_points(&self.groups)
799 }
800
801 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
803 self.ensure_graph_built();
804 self.topo_graph
805 .lock()
806 .ok()
807 .and_then(|g| g.shortest_route(from, to))
808 }
809}