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> {
230 if !params.min_position.is_finite() || !params.max_position.is_finite() {
231 return Err(SimError::InvalidConfig {
232 field: "line.range",
233 reason: format!(
234 "min/max must be finite (got min={}, max={})",
235 params.min_position, params.max_position
236 ),
237 });
238 }
239 if params.min_position > params.max_position {
240 return Err(SimError::InvalidConfig {
241 field: "line.range",
242 reason: format!(
243 "min ({}) must be <= max ({})",
244 params.min_position, params.max_position
245 ),
246 });
247 }
248
249 let group_id = params.group;
250 let group = self
251 .groups
252 .iter_mut()
253 .find(|g| g.id() == group_id)
254 .ok_or(SimError::GroupNotFound(group_id))?;
255
256 let line_tag = format!("line:{}", params.name);
257
258 let eid = self.world.spawn();
259 self.world.set_line(
260 eid,
261 Line {
262 name: params.name.clone(),
263 group: group_id,
264 orientation: params.orientation,
265 position: params.position,
266 min_position: params.min_position,
267 max_position: params.max_position,
268 max_cars: params.max_cars,
269 },
270 );
271
272 group
273 .lines_mut()
274 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
275
276 if let Some(tags) = self
278 .world
279 .resource_mut::<crate::tagged_metrics::MetricTags>()
280 {
281 tags.tag(eid, line_tag);
282 }
283
284 self.mark_topo_dirty();
285 self.events.emit(Event::LineAdded {
286 line: eid,
287 group: group_id,
288 tick: self.tick,
289 });
290 Ok(eid)
291 }
292
293 pub fn set_line_range(&mut self, line: EntityId, min: f64, max: f64) -> Result<(), SimError> {
306 if !min.is_finite() || !max.is_finite() {
307 return Err(SimError::InvalidConfig {
308 field: "line.range",
309 reason: format!("min/max must be finite (got min={min}, max={max})"),
310 });
311 }
312 if min > max {
313 return Err(SimError::InvalidConfig {
314 field: "line.range",
315 reason: format!("min ({min}) must be <= max ({max})"),
316 });
317 }
318 let line_ref = self
319 .world
320 .line_mut(line)
321 .ok_or(SimError::LineNotFound(line))?;
322 line_ref.min_position = min;
323 line_ref.max_position = max;
324
325 let car_ids: Vec<EntityId> = self
327 .world
328 .iter_elevators()
329 .filter_map(|(eid, _, car)| (car.line == line).then_some(eid))
330 .collect();
331 for eid in car_ids {
332 let Some(pos) = self.world.position(eid).map(|p| p.value) else {
336 continue;
337 };
338 if pos < min || pos > max {
339 let clamped = pos.clamp(min, max);
340 if let Some(p) = self.world.position_mut(eid) {
341 p.value = clamped;
342 }
343 if let Some(v) = self.world.velocity_mut(eid) {
344 v.value = 0.0;
345 }
346 }
347 }
348
349 self.mark_topo_dirty();
350 Ok(())
351 }
352
353 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
363 let (group_idx, line_idx) = self.find_line(line)?;
364
365 let group_id = self.groups[group_idx].id();
366
367 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
369 .elevators()
370 .to_vec();
371
372 for eid in &elevator_ids {
374 let _ = self.disable(*eid);
376 }
377
378 self.groups[group_idx].lines_mut().remove(line_idx);
380
381 self.groups[group_idx].rebuild_caches();
383
384 self.world.remove_line(line);
386
387 self.mark_topo_dirty();
388 self.events.emit(Event::LineRemoved {
389 line,
390 group: group_id,
391 tick: self.tick,
392 });
393 Ok(())
394 }
395
396 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
405 let line = self
406 .world
407 .elevator(elevator)
408 .ok_or(SimError::EntityNotFound(elevator))?
409 .line();
410
411 let _ = self.disable(elevator);
413
414 let resolved_group: Option<GroupId> = match self.find_line(line) {
425 Ok((group_idx, line_idx)) => {
426 self.groups[group_idx].lines_mut()[line_idx]
427 .elevators_mut()
428 .retain(|&e| e != elevator);
429 self.groups[group_idx].rebuild_caches();
430 Some(self.groups[group_idx].id())
431 }
432 Err(_) => None,
433 };
434
435 if let Some(group_id) = resolved_group {
439 self.events.emit(Event::ElevatorRemoved {
440 elevator,
441 line,
442 group: group_id,
443 tick: self.tick,
444 });
445 }
446
447 self.world.despawn(elevator);
449
450 self.mark_topo_dirty();
451 Ok(())
452 }
453
454 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
463 if self.world.stop(stop).is_none() {
464 return Err(SimError::EntityNotFound(stop));
465 }
466
467 let residents: Vec<EntityId> = self
470 .rider_index
471 .residents_at(stop)
472 .iter()
473 .copied()
474 .collect();
475 if !residents.is_empty() {
476 self.events
477 .emit(Event::ResidentsAtRemovedStop { stop, residents });
478 }
479
480 self.disable_stop_inner(stop, true);
484 self.world.disable(stop);
485 self.events.emit(Event::EntityDisabled {
486 entity: stop,
487 tick: self.tick,
488 });
489
490 let elevator_ids: Vec<EntityId> =
494 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
495 for eid in elevator_ids {
496 if let Some(car) = self.world.elevator_mut(eid) {
497 if car.target_stop == Some(stop) {
498 car.target_stop = None;
499 }
500 car.restricted_stops.remove(&stop);
501 }
502 if let Some(q) = self.world.destination_queue_mut(eid) {
503 q.retain(|s| s != stop);
504 }
505 if let Some(calls) = self.world.car_calls_mut(eid) {
510 calls.retain(|c| c.floor != stop);
511 }
512 }
513
514 for group in &mut self.groups {
516 for line_info in group.lines_mut() {
517 line_info.serves_mut().retain(|&s| s != stop);
518 }
519 group.rebuild_caches();
520 }
521
522 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
524 sorted.0.retain(|&(_, s)| s != stop);
525 }
526
527 self.stop_lookup.retain(|_, &mut eid| eid != stop);
529
530 self.events.emit(Event::StopRemoved {
531 stop,
532 tick: self.tick,
533 });
534
535 self.world.despawn(stop);
537
538 self.rider_index.rebuild(&self.world);
542
543 self.mark_topo_dirty();
544 Ok(())
545 }
546
547 pub fn add_group(
549 &mut self,
550 name: impl Into<String>,
551 dispatch: impl DispatchStrategy + 'static,
552 ) -> GroupId {
553 let next_id = self
554 .groups
555 .iter()
556 .map(|g| g.id().0)
557 .max()
558 .map_or(0, |m| m + 1);
559 let group_id = GroupId(next_id);
560
561 self.groups
562 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
563
564 self.dispatchers.insert(group_id, Box::new(dispatch));
565 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
566 self.mark_topo_dirty();
567 group_id
568 }
569
570 pub fn assign_line_to_group(
577 &mut self,
578 line: EntityId,
579 new_group: GroupId,
580 ) -> Result<GroupId, SimError> {
581 let (old_group_idx, line_idx) = self.find_line(line)?;
582
583 if !self.groups.iter().any(|g| g.id() == new_group) {
585 return Err(SimError::GroupNotFound(new_group));
586 }
587
588 let old_group_id = self.groups[old_group_idx].id();
589
590 if old_group_id == new_group {
595 return Ok(old_group_id);
596 }
597
598 let elevators_to_notify: Vec<EntityId> = self.groups[old_group_idx].lines()[line_idx]
603 .elevators()
604 .to_vec();
605 if let Some(dispatcher) = self.dispatchers.get_mut(&old_group_id) {
606 for eid in &elevators_to_notify {
607 dispatcher.notify_removed(*eid);
608 }
609 }
610
611 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
613 self.groups[old_group_idx].rebuild_caches();
614
615 let new_group_idx = self
620 .groups
621 .iter()
622 .position(|g| g.id() == new_group)
623 .ok_or(SimError::GroupNotFound(new_group))?;
624 self.groups[new_group_idx].lines_mut().push(line_info);
625 self.groups[new_group_idx].rebuild_caches();
626
627 if let Some(line_comp) = self.world.line_mut(line) {
629 line_comp.group = new_group;
630 }
631
632 self.mark_topo_dirty();
633 self.events.emit(Event::LineReassigned {
634 line,
635 old_group: old_group_id,
636 new_group,
637 tick: self.tick,
638 });
639
640 Ok(old_group_id)
641 }
642
643 pub fn reassign_elevator_to_line(
654 &mut self,
655 elevator: EntityId,
656 new_line: EntityId,
657 ) -> Result<(), SimError> {
658 let old_line = self
659 .world
660 .elevator(elevator)
661 .ok_or(SimError::EntityNotFound(elevator))?
662 .line();
663
664 if old_line == new_line {
665 return Ok(());
666 }
667
668 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
670 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
671
672 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
674 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
675 .elevators()
676 .len();
677 if current_count >= max {
678 return Err(SimError::InvalidConfig {
679 field: "line.max_cars",
680 reason: format!("target line already has {current_count} cars (max {max})"),
681 });
682 }
683 }
684
685 let old_group_id = self.groups[old_group_idx].id();
686 let new_group_id = self.groups[new_group_idx].id();
687
688 self.groups[old_group_idx].lines_mut()[old_line_idx]
689 .elevators_mut()
690 .retain(|&e| e != elevator);
691 self.groups[new_group_idx].lines_mut()[new_line_idx]
692 .elevators_mut()
693 .push(elevator);
694
695 if let Some(car) = self.world.elevator_mut(elevator) {
696 car.line = new_line;
697 }
698
699 self.groups[old_group_idx].rebuild_caches();
700 if new_group_idx != old_group_idx {
701 self.groups[new_group_idx].rebuild_caches();
702
703 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
707 old_dispatcher.notify_removed(elevator);
708 }
709 }
710
711 self.mark_topo_dirty();
712
713 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
715 elevator,
716 old_line,
717 new_line,
718 tick: self.tick,
719 });
720
721 Ok(())
722 }
723
724 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
731 if self.world.stop(stop).is_none() {
733 return Err(SimError::EntityNotFound(stop));
734 }
735
736 let (group_idx, line_idx) = self.find_line(line)?;
737
738 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
739 if !li.serves().contains(&stop) {
740 li.serves_mut().push(stop);
741 }
742
743 self.groups[group_idx].push_stop(stop);
744
745 self.mark_topo_dirty();
746 Ok(())
747 }
748
749 pub fn remove_stop_from_line(
755 &mut self,
756 stop: EntityId,
757 line: EntityId,
758 ) -> Result<(), SimError> {
759 let (group_idx, line_idx) = self.find_line(line)?;
760
761 self.groups[group_idx].lines_mut()[line_idx]
762 .serves_mut()
763 .retain(|&s| s != stop);
764
765 self.groups[group_idx].rebuild_caches();
767
768 self.mark_topo_dirty();
769 Ok(())
770 }
771
772 #[must_use]
776 pub fn all_lines(&self) -> Vec<EntityId> {
777 self.groups
778 .iter()
779 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
780 .collect()
781 }
782
783 #[must_use]
785 pub fn line_count(&self) -> usize {
786 self.groups.iter().map(|g| g.lines().len()).sum()
787 }
788
789 #[must_use]
791 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
792 self.groups
793 .iter()
794 .find(|g| g.id() == group)
795 .map_or_else(Vec::new, |g| {
796 g.lines().iter().map(LineInfo::entity).collect()
797 })
798 }
799
800 #[must_use]
802 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
803 self.groups
804 .iter()
805 .flat_map(ElevatorGroup::lines)
806 .find(|li| li.entity() == line)
807 .map_or_else(Vec::new, |li| li.elevators().to_vec())
808 }
809
810 #[must_use]
812 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
813 self.groups
814 .iter()
815 .flat_map(ElevatorGroup::lines)
816 .find(|li| li.entity() == line)
817 .map_or_else(Vec::new, |li| li.serves().to_vec())
818 }
819
820 #[must_use]
822 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
823 self.groups
824 .iter()
825 .flat_map(ElevatorGroup::lines)
826 .find(|li| li.elevators().contains(&elevator))
827 .map(LineInfo::entity)
828 }
829
830 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
832 self.world
833 .iter_elevators()
834 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
835 }
836
837 #[must_use]
839 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
840 self.groups
841 .iter()
842 .flat_map(ElevatorGroup::lines)
843 .filter(|li| li.serves().contains(&stop))
844 .map(LineInfo::entity)
845 .collect()
846 }
847
848 #[must_use]
850 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
851 self.groups
852 .iter()
853 .filter(|g| g.stop_entities().contains(&stop))
854 .map(ElevatorGroup::id)
855 .collect()
856 }
857
858 fn ensure_graph_built(&self) {
862 if let Ok(mut graph) = self.topo_graph.lock()
863 && graph.is_dirty()
864 {
865 graph.rebuild(&self.groups);
866 }
867 }
868
869 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
871 self.ensure_graph_built();
872 self.topo_graph
873 .lock()
874 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
875 }
876
877 pub fn transfer_points(&self) -> Vec<EntityId> {
879 self.ensure_graph_built();
880 TopologyGraph::transfer_points(&self.groups)
881 }
882
883 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
885 self.ensure_graph_built();
886 self.topo_graph
887 .lock()
888 .ok()
889 .and_then(|g| g.shortest_route(from, to))
890 }
891}