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].add_stop(eid);
82
83 self.groups[group_idx].push_stop(eid);
85
86 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
88 let idx = sorted.0.partition_point(|&(p, _)| p < position);
89 sorted.0.insert(idx, (position, eid));
90 }
91
92 self.mark_topo_dirty();
93 self.events.emit(Event::StopAdded {
94 stop: eid,
95 line,
96 group: group_id,
97 tick: self.tick,
98 });
99 Ok(eid)
100 }
101
102 pub fn add_elevator(
108 &mut self,
109 params: &ElevatorParams,
110 line: EntityId,
111 starting_position: f64,
112 ) -> Result<EntityId, SimError> {
113 super::construction::validate_elevator_physics(
116 params.max_speed.value(),
117 params.acceleration.value(),
118 params.deceleration.value(),
119 params.weight_capacity.value(),
120 params.inspection_speed_factor,
121 params.door_transition_ticks,
122 params.door_open_ticks,
123 params.bypass_load_up_pct,
124 params.bypass_load_down_pct,
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 bypass_load_up_pct: params.bypass_load_up_pct,
187 bypass_load_down_pct: params.bypass_load_down_pct,
188 home_stop: None,
189 },
190 );
191 self.world
192 .set_destination_queue(eid, crate::components::DestinationQueue::new());
193 self.groups[group_idx].lines_mut()[line_idx].add_elevator(eid);
194 self.groups[group_idx].push_elevator(eid);
195
196 let line_name = self.world.line(line).map(|l| l.name.clone());
198 if let Some(name) = line_name
199 && let Some(tags) = self
200 .world
201 .resource_mut::<crate::tagged_metrics::MetricTags>()
202 {
203 tags.tag(eid, format!("line:{name}"));
204 }
205
206 self.mark_topo_dirty();
207 self.events.emit(Event::ElevatorAdded {
208 elevator: eid,
209 line,
210 group: group_id,
211 tick: self.tick,
212 });
213 Ok(eid)
214 }
215
216 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
227 if !params.min_position.is_finite() || !params.max_position.is_finite() {
228 return Err(SimError::InvalidConfig {
229 field: "line.range",
230 reason: format!(
231 "min/max must be finite (got min={}, max={})",
232 params.min_position, params.max_position
233 ),
234 });
235 }
236 if params.min_position > params.max_position {
237 return Err(SimError::InvalidConfig {
238 field: "line.range",
239 reason: format!(
240 "min ({}) must be <= max ({})",
241 params.min_position, params.max_position
242 ),
243 });
244 }
245
246 let group_id = params.group;
247 let group = self
248 .groups
249 .iter_mut()
250 .find(|g| g.id() == group_id)
251 .ok_or(SimError::GroupNotFound(group_id))?;
252
253 let line_tag = format!("line:{}", params.name);
254
255 let eid = self.world.spawn();
256 self.world.set_line(
257 eid,
258 Line {
259 name: params.name.clone(),
260 group: group_id,
261 orientation: params.orientation,
262 position: params.position,
263 min_position: params.min_position,
264 max_position: params.max_position,
265 max_cars: params.max_cars,
266 },
267 );
268
269 group
270 .lines_mut()
271 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
272
273 if let Some(tags) = self
275 .world
276 .resource_mut::<crate::tagged_metrics::MetricTags>()
277 {
278 tags.tag(eid, line_tag);
279 }
280
281 self.mark_topo_dirty();
282 self.events.emit(Event::LineAdded {
283 line: eid,
284 group: group_id,
285 tick: self.tick,
286 });
287 Ok(eid)
288 }
289
290 pub fn set_line_range(&mut self, line: EntityId, min: f64, max: f64) -> Result<(), SimError> {
303 if !min.is_finite() || !max.is_finite() {
304 return Err(SimError::InvalidConfig {
305 field: "line.range",
306 reason: format!("min/max must be finite (got min={min}, max={max})"),
307 });
308 }
309 if min > max {
310 return Err(SimError::InvalidConfig {
311 field: "line.range",
312 reason: format!("min ({min}) must be <= max ({max})"),
313 });
314 }
315 let line_ref = self
316 .world
317 .line_mut(line)
318 .ok_or(SimError::LineNotFound(line))?;
319 line_ref.min_position = min;
320 line_ref.max_position = max;
321
322 let car_ids: Vec<EntityId> = self
324 .world
325 .iter_elevators()
326 .filter_map(|(eid, _, car)| (car.line == line).then_some(eid))
327 .collect();
328 for eid in car_ids {
329 let Some(pos) = self.world.position(eid).map(|p| p.value) else {
333 continue;
334 };
335 if pos < min || pos > max {
336 let clamped = pos.clamp(min, max);
337 if let Some(p) = self.world.position_mut(eid) {
338 p.value = clamped;
339 }
340 if let Some(v) = self.world.velocity_mut(eid) {
341 v.value = 0.0;
342 }
343 }
344 }
345
346 self.mark_topo_dirty();
347 Ok(())
348 }
349
350 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
360 let (group_idx, line_idx) = self.find_line(line)?;
361
362 let group_id = self.groups[group_idx].id();
363
364 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
366 .elevators()
367 .to_vec();
368
369 for eid in &elevator_ids {
371 let _ = self.disable(*eid);
373 }
374
375 self.groups[group_idx].lines_mut().remove(line_idx);
377
378 self.groups[group_idx].rebuild_caches();
380
381 self.world.remove_line(line);
383
384 self.mark_topo_dirty();
385 self.events.emit(Event::LineRemoved {
386 line,
387 group: group_id,
388 tick: self.tick,
389 });
390 Ok(())
391 }
392
393 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
402 let line = self
403 .world
404 .elevator(elevator)
405 .ok_or(SimError::EntityNotFound(elevator))?
406 .line();
407
408 let _ = self.disable(elevator);
410
411 let resolved_group: Option<GroupId> = match self.find_line(line) {
422 Ok((group_idx, line_idx)) => {
423 self.groups[group_idx].lines_mut()[line_idx].remove_elevator(elevator);
424 self.groups[group_idx].rebuild_caches();
425 Some(self.groups[group_idx].id())
426 }
427 Err(_) => None,
428 };
429
430 if let Some(group_id) = resolved_group {
434 self.events.emit(Event::ElevatorRemoved {
435 elevator,
436 line,
437 group: group_id,
438 tick: self.tick,
439 });
440 }
441
442 self.world.despawn(elevator);
444
445 self.mark_topo_dirty();
446 Ok(())
447 }
448
449 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
458 if self.world.stop(stop).is_none() {
459 return Err(SimError::EntityNotFound(stop));
460 }
461
462 let residents: Vec<EntityId> = self
465 .rider_index
466 .residents_at(stop)
467 .iter()
468 .copied()
469 .collect();
470 if !residents.is_empty() {
471 self.events
472 .emit(Event::ResidentsAtRemovedStop { stop, residents });
473 }
474
475 self.disable_stop_inner(stop, true);
479 self.world.disable(stop);
480 self.events.emit(Event::EntityDisabled {
481 entity: stop,
482 tick: self.tick,
483 });
484
485 let elevator_ids: Vec<EntityId> =
489 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
490 for eid in elevator_ids {
491 if let Some(car) = self.world.elevator_mut(eid) {
492 if car.target_stop == Some(stop) {
493 car.target_stop = None;
494 }
495 car.restricted_stops.remove(&stop);
496 }
497 if let Some(q) = self.world.destination_queue_mut(eid) {
498 q.retain(|s| s != stop);
499 }
500 if let Some(calls) = self.world.car_calls_mut(eid) {
505 calls.retain(|c| c.floor != stop);
506 }
507 }
508
509 for group in &mut self.groups {
511 for line_info in group.lines_mut() {
512 line_info.remove_stop(stop);
513 }
514 group.rebuild_caches();
515 }
516
517 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
519 sorted.0.retain(|&(_, s)| s != stop);
520 }
521
522 self.stop_lookup.retain(|_, &mut eid| eid != stop);
524
525 self.events.emit(Event::StopRemoved {
526 stop,
527 tick: self.tick,
528 });
529
530 self.world.despawn(stop);
532
533 self.rider_index.rebuild(&self.world);
537
538 self.mark_topo_dirty();
539 Ok(())
540 }
541
542 pub fn add_group(
544 &mut self,
545 name: impl Into<String>,
546 dispatch: impl DispatchStrategy + 'static,
547 ) -> GroupId {
548 let next_id = self
549 .groups
550 .iter()
551 .map(|g| g.id().0)
552 .max()
553 .map_or(0, |m| m + 1);
554 let group_id = GroupId(next_id);
555
556 self.groups
557 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
558
559 self.dispatcher_set
560 .insert(group_id, Box::new(dispatch), BuiltinStrategy::Scan);
561 self.mark_topo_dirty();
562 group_id
563 }
564
565 pub fn assign_line_to_group(
572 &mut self,
573 line: EntityId,
574 new_group: GroupId,
575 ) -> Result<GroupId, SimError> {
576 let (old_group_idx, line_idx) = self.find_line(line)?;
577
578 if !self.groups.iter().any(|g| g.id() == new_group) {
580 return Err(SimError::GroupNotFound(new_group));
581 }
582
583 let old_group_id = self.groups[old_group_idx].id();
584
585 if old_group_id == new_group {
590 return Ok(old_group_id);
591 }
592
593 let elevators_to_notify: Vec<EntityId> = self.groups[old_group_idx].lines()[line_idx]
598 .elevators()
599 .to_vec();
600 if let Some(dispatcher) = self.dispatcher_set.strategies_mut().get_mut(&old_group_id) {
601 for eid in &elevators_to_notify {
602 dispatcher.notify_removed(*eid);
603 }
604 }
605
606 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
608 self.groups[old_group_idx].rebuild_caches();
609
610 let new_group_idx = self
615 .groups
616 .iter()
617 .position(|g| g.id() == new_group)
618 .ok_or(SimError::GroupNotFound(new_group))?;
619 self.groups[new_group_idx].lines_mut().push(line_info);
620 self.groups[new_group_idx].rebuild_caches();
621
622 if let Some(line_comp) = self.world.line_mut(line) {
624 line_comp.group = new_group;
625 }
626
627 self.mark_topo_dirty();
628 self.events.emit(Event::LineReassigned {
629 line,
630 old_group: old_group_id,
631 new_group,
632 tick: self.tick,
633 });
634
635 Ok(old_group_id)
636 }
637
638 pub fn reassign_elevator_to_line(
649 &mut self,
650 elevator: EntityId,
651 new_line: EntityId,
652 ) -> Result<(), SimError> {
653 let old_line = self
654 .world
655 .elevator(elevator)
656 .ok_or(SimError::EntityNotFound(elevator))?
657 .line();
658
659 if old_line == new_line {
660 return Ok(());
661 }
662
663 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
665 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
666
667 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
669 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
670 .elevators()
671 .len();
672 if current_count >= max {
673 return Err(SimError::InvalidConfig {
674 field: "line.max_cars",
675 reason: format!("target line already has {current_count} cars (max {max})"),
676 });
677 }
678 }
679
680 let old_group_id = self.groups[old_group_idx].id();
681 let new_group_id = self.groups[new_group_idx].id();
682
683 self.groups[old_group_idx].lines_mut()[old_line_idx].remove_elevator(elevator);
684 self.groups[new_group_idx].lines_mut()[new_line_idx].add_elevator(elevator);
685
686 if let Some(car) = self.world.elevator_mut(elevator) {
687 car.line = new_line;
688 }
689
690 self.groups[old_group_idx].rebuild_caches();
691 if new_group_idx != old_group_idx {
692 self.groups[new_group_idx].rebuild_caches();
693
694 if let Some(old_dispatcher) =
698 self.dispatcher_set.strategies_mut().get_mut(&old_group_id)
699 {
700 old_dispatcher.notify_removed(elevator);
701 }
702 }
703
704 self.mark_topo_dirty();
705
706 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
708 elevator,
709 old_line,
710 new_line,
711 tick: self.tick,
712 });
713
714 Ok(())
715 }
716
717 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
724 if self.world.stop(stop).is_none() {
726 return Err(SimError::EntityNotFound(stop));
727 }
728
729 let (group_idx, line_idx) = self.find_line(line)?;
730
731 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
732 li.add_stop(stop);
733
734 self.groups[group_idx].push_stop(stop);
735
736 self.mark_topo_dirty();
737 Ok(())
738 }
739
740 pub fn remove_stop_from_line(
746 &mut self,
747 stop: EntityId,
748 line: EntityId,
749 ) -> Result<(), SimError> {
750 let (group_idx, line_idx) = self.find_line(line)?;
751
752 self.groups[group_idx].lines_mut()[line_idx].remove_stop(stop);
753
754 self.groups[group_idx].rebuild_caches();
756
757 self.mark_topo_dirty();
758 Ok(())
759 }
760
761 #[must_use]
765 pub fn all_lines(&self) -> Vec<EntityId> {
766 self.groups
767 .iter()
768 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
769 .collect()
770 }
771
772 #[must_use]
774 pub fn line_count(&self) -> usize {
775 self.groups.iter().map(|g| g.lines().len()).sum()
776 }
777
778 #[must_use]
780 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
781 self.groups
782 .iter()
783 .find(|g| g.id() == group)
784 .map_or_else(Vec::new, |g| {
785 g.lines().iter().map(LineInfo::entity).collect()
786 })
787 }
788
789 #[must_use]
791 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
792 self.groups
793 .iter()
794 .flat_map(ElevatorGroup::lines)
795 .find(|li| li.entity() == line)
796 .map_or_else(Vec::new, |li| li.elevators().to_vec())
797 }
798
799 #[must_use]
801 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
802 self.groups
803 .iter()
804 .flat_map(ElevatorGroup::lines)
805 .find(|li| li.entity() == line)
806 .map_or_else(Vec::new, |li| li.serves().to_vec())
807 }
808
809 #[must_use]
822 pub fn find_stop_at_position_on_line(&self, position: f64, line: EntityId) -> Option<EntityId> {
823 let line_info = self
824 .groups
825 .iter()
826 .flat_map(ElevatorGroup::lines)
827 .find(|li| li.entity() == line)?;
828 self.world
829 .find_stop_at_position_in(position, line_info.serves())
830 }
831
832 #[must_use]
834 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
835 self.groups
836 .iter()
837 .flat_map(ElevatorGroup::lines)
838 .find(|li| li.elevators().contains(&elevator))
839 .map(LineInfo::entity)
840 }
841
842 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
844 self.world
845 .iter_elevators()
846 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
847 }
848
849 #[must_use]
851 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
852 self.groups
853 .iter()
854 .flat_map(ElevatorGroup::lines)
855 .filter(|li| li.serves().contains(&stop))
856 .map(LineInfo::entity)
857 .collect()
858 }
859
860 #[must_use]
862 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
863 self.groups
864 .iter()
865 .filter(|g| g.stop_entities().contains(&stop))
866 .map(ElevatorGroup::id)
867 .collect()
868 }
869
870 fn ensure_graph_built(&self) {
874 if let Ok(mut graph) = self.topo_graph.lock()
875 && graph.is_dirty()
876 {
877 graph.rebuild(&self.groups);
878 }
879 }
880
881 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
883 self.ensure_graph_built();
884 self.topo_graph
885 .lock()
886 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
887 }
888
889 pub fn transfer_points(&self) -> Vec<EntityId> {
891 self.ensure_graph_built();
892 TopologyGraph::transfer_points(&self.groups)
893 }
894
895 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
897 self.ensure_graph_built();
898 self.topo_graph
899 .lock()
900 .ok()
901 .and_then(|g| g.shortest_route(from, to))
902 }
903}