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 home_stop: None,
191 },
192 );
193 self.world
194 .set_destination_queue(eid, crate::components::DestinationQueue::new());
195 self.groups[group_idx].lines_mut()[line_idx]
196 .elevators_mut()
197 .push(eid);
198 self.groups[group_idx].push_elevator(eid);
199
200 let line_name = self.world.line(line).map(|l| l.name.clone());
202 if let Some(name) = line_name
203 && let Some(tags) = self
204 .world
205 .resource_mut::<crate::tagged_metrics::MetricTags>()
206 {
207 tags.tag(eid, format!("line:{name}"));
208 }
209
210 self.mark_topo_dirty();
211 self.events.emit(Event::ElevatorAdded {
212 elevator: eid,
213 line,
214 group: group_id,
215 tick: self.tick,
216 });
217 Ok(eid)
218 }
219
220 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
231 if !params.min_position.is_finite() || !params.max_position.is_finite() {
232 return Err(SimError::InvalidConfig {
233 field: "line.range",
234 reason: format!(
235 "min/max must be finite (got min={}, max={})",
236 params.min_position, params.max_position
237 ),
238 });
239 }
240 if params.min_position > params.max_position {
241 return Err(SimError::InvalidConfig {
242 field: "line.range",
243 reason: format!(
244 "min ({}) must be <= max ({})",
245 params.min_position, params.max_position
246 ),
247 });
248 }
249
250 let group_id = params.group;
251 let group = self
252 .groups
253 .iter_mut()
254 .find(|g| g.id() == group_id)
255 .ok_or(SimError::GroupNotFound(group_id))?;
256
257 let line_tag = format!("line:{}", params.name);
258
259 let eid = self.world.spawn();
260 self.world.set_line(
261 eid,
262 Line {
263 name: params.name.clone(),
264 group: group_id,
265 orientation: params.orientation,
266 position: params.position,
267 min_position: params.min_position,
268 max_position: params.max_position,
269 max_cars: params.max_cars,
270 },
271 );
272
273 group
274 .lines_mut()
275 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
276
277 if let Some(tags) = self
279 .world
280 .resource_mut::<crate::tagged_metrics::MetricTags>()
281 {
282 tags.tag(eid, line_tag);
283 }
284
285 self.mark_topo_dirty();
286 self.events.emit(Event::LineAdded {
287 line: eid,
288 group: group_id,
289 tick: self.tick,
290 });
291 Ok(eid)
292 }
293
294 pub fn set_line_range(&mut self, line: EntityId, min: f64, max: f64) -> Result<(), SimError> {
307 if !min.is_finite() || !max.is_finite() {
308 return Err(SimError::InvalidConfig {
309 field: "line.range",
310 reason: format!("min/max must be finite (got min={min}, max={max})"),
311 });
312 }
313 if min > max {
314 return Err(SimError::InvalidConfig {
315 field: "line.range",
316 reason: format!("min ({min}) must be <= max ({max})"),
317 });
318 }
319 let line_ref = self
320 .world
321 .line_mut(line)
322 .ok_or(SimError::LineNotFound(line))?;
323 line_ref.min_position = min;
324 line_ref.max_position = max;
325
326 let car_ids: Vec<EntityId> = self
328 .world
329 .iter_elevators()
330 .filter_map(|(eid, _, car)| (car.line == line).then_some(eid))
331 .collect();
332 for eid in car_ids {
333 let Some(pos) = self.world.position(eid).map(|p| p.value) else {
337 continue;
338 };
339 if pos < min || pos > max {
340 let clamped = pos.clamp(min, max);
341 if let Some(p) = self.world.position_mut(eid) {
342 p.value = clamped;
343 }
344 if let Some(v) = self.world.velocity_mut(eid) {
345 v.value = 0.0;
346 }
347 }
348 }
349
350 self.mark_topo_dirty();
351 Ok(())
352 }
353
354 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
364 let (group_idx, line_idx) = self.find_line(line)?;
365
366 let group_id = self.groups[group_idx].id();
367
368 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
370 .elevators()
371 .to_vec();
372
373 for eid in &elevator_ids {
375 let _ = self.disable(*eid);
377 }
378
379 self.groups[group_idx].lines_mut().remove(line_idx);
381
382 self.groups[group_idx].rebuild_caches();
384
385 self.world.remove_line(line);
387
388 self.mark_topo_dirty();
389 self.events.emit(Event::LineRemoved {
390 line,
391 group: group_id,
392 tick: self.tick,
393 });
394 Ok(())
395 }
396
397 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
406 let line = self
407 .world
408 .elevator(elevator)
409 .ok_or(SimError::EntityNotFound(elevator))?
410 .line();
411
412 let _ = self.disable(elevator);
414
415 let resolved_group: Option<GroupId> = match self.find_line(line) {
426 Ok((group_idx, line_idx)) => {
427 self.groups[group_idx].lines_mut()[line_idx]
428 .elevators_mut()
429 .retain(|&e| e != elevator);
430 self.groups[group_idx].rebuild_caches();
431 Some(self.groups[group_idx].id())
432 }
433 Err(_) => None,
434 };
435
436 if let Some(group_id) = resolved_group {
440 self.events.emit(Event::ElevatorRemoved {
441 elevator,
442 line,
443 group: group_id,
444 tick: self.tick,
445 });
446 }
447
448 self.world.despawn(elevator);
450
451 self.mark_topo_dirty();
452 Ok(())
453 }
454
455 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
464 if self.world.stop(stop).is_none() {
465 return Err(SimError::EntityNotFound(stop));
466 }
467
468 let residents: Vec<EntityId> = self
471 .rider_index
472 .residents_at(stop)
473 .iter()
474 .copied()
475 .collect();
476 if !residents.is_empty() {
477 self.events
478 .emit(Event::ResidentsAtRemovedStop { stop, residents });
479 }
480
481 self.disable_stop_inner(stop, true);
485 self.world.disable(stop);
486 self.events.emit(Event::EntityDisabled {
487 entity: stop,
488 tick: self.tick,
489 });
490
491 let elevator_ids: Vec<EntityId> =
495 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
496 for eid in elevator_ids {
497 if let Some(car) = self.world.elevator_mut(eid) {
498 if car.target_stop == Some(stop) {
499 car.target_stop = None;
500 }
501 car.restricted_stops.remove(&stop);
502 }
503 if let Some(q) = self.world.destination_queue_mut(eid) {
504 q.retain(|s| s != stop);
505 }
506 if let Some(calls) = self.world.car_calls_mut(eid) {
511 calls.retain(|c| c.floor != stop);
512 }
513 }
514
515 for group in &mut self.groups {
517 for line_info in group.lines_mut() {
518 line_info.serves_mut().retain(|&s| s != stop);
519 }
520 group.rebuild_caches();
521 }
522
523 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
525 sorted.0.retain(|&(_, s)| s != stop);
526 }
527
528 self.stop_lookup.retain(|_, &mut eid| eid != stop);
530
531 self.events.emit(Event::StopRemoved {
532 stop,
533 tick: self.tick,
534 });
535
536 self.world.despawn(stop);
538
539 self.rider_index.rebuild(&self.world);
543
544 self.mark_topo_dirty();
545 Ok(())
546 }
547
548 pub fn add_group(
550 &mut self,
551 name: impl Into<String>,
552 dispatch: impl DispatchStrategy + 'static,
553 ) -> GroupId {
554 let next_id = self
555 .groups
556 .iter()
557 .map(|g| g.id().0)
558 .max()
559 .map_or(0, |m| m + 1);
560 let group_id = GroupId(next_id);
561
562 self.groups
563 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
564
565 self.dispatchers.insert(group_id, Box::new(dispatch));
566 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
567 self.mark_topo_dirty();
568 group_id
569 }
570
571 pub fn assign_line_to_group(
578 &mut self,
579 line: EntityId,
580 new_group: GroupId,
581 ) -> Result<GroupId, SimError> {
582 let (old_group_idx, line_idx) = self.find_line(line)?;
583
584 if !self.groups.iter().any(|g| g.id() == new_group) {
586 return Err(SimError::GroupNotFound(new_group));
587 }
588
589 let old_group_id = self.groups[old_group_idx].id();
590
591 if old_group_id == new_group {
596 return Ok(old_group_id);
597 }
598
599 let elevators_to_notify: Vec<EntityId> = self.groups[old_group_idx].lines()[line_idx]
604 .elevators()
605 .to_vec();
606 if let Some(dispatcher) = self.dispatchers.get_mut(&old_group_id) {
607 for eid in &elevators_to_notify {
608 dispatcher.notify_removed(*eid);
609 }
610 }
611
612 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
614 self.groups[old_group_idx].rebuild_caches();
615
616 let new_group_idx = self
621 .groups
622 .iter()
623 .position(|g| g.id() == new_group)
624 .ok_or(SimError::GroupNotFound(new_group))?;
625 self.groups[new_group_idx].lines_mut().push(line_info);
626 self.groups[new_group_idx].rebuild_caches();
627
628 if let Some(line_comp) = self.world.line_mut(line) {
630 line_comp.group = new_group;
631 }
632
633 self.mark_topo_dirty();
634 self.events.emit(Event::LineReassigned {
635 line,
636 old_group: old_group_id,
637 new_group,
638 tick: self.tick,
639 });
640
641 Ok(old_group_id)
642 }
643
644 pub fn reassign_elevator_to_line(
655 &mut self,
656 elevator: EntityId,
657 new_line: EntityId,
658 ) -> Result<(), SimError> {
659 let old_line = self
660 .world
661 .elevator(elevator)
662 .ok_or(SimError::EntityNotFound(elevator))?
663 .line();
664
665 if old_line == new_line {
666 return Ok(());
667 }
668
669 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
671 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
672
673 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
675 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
676 .elevators()
677 .len();
678 if current_count >= max {
679 return Err(SimError::InvalidConfig {
680 field: "line.max_cars",
681 reason: format!("target line already has {current_count} cars (max {max})"),
682 });
683 }
684 }
685
686 let old_group_id = self.groups[old_group_idx].id();
687 let new_group_id = self.groups[new_group_idx].id();
688
689 self.groups[old_group_idx].lines_mut()[old_line_idx]
690 .elevators_mut()
691 .retain(|&e| e != elevator);
692 self.groups[new_group_idx].lines_mut()[new_line_idx]
693 .elevators_mut()
694 .push(elevator);
695
696 if let Some(car) = self.world.elevator_mut(elevator) {
697 car.line = new_line;
698 }
699
700 self.groups[old_group_idx].rebuild_caches();
701 if new_group_idx != old_group_idx {
702 self.groups[new_group_idx].rebuild_caches();
703
704 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
708 old_dispatcher.notify_removed(elevator);
709 }
710 }
711
712 self.mark_topo_dirty();
713
714 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
716 elevator,
717 old_line,
718 new_line,
719 tick: self.tick,
720 });
721
722 Ok(())
723 }
724
725 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
732 if self.world.stop(stop).is_none() {
734 return Err(SimError::EntityNotFound(stop));
735 }
736
737 let (group_idx, line_idx) = self.find_line(line)?;
738
739 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
740 if !li.serves().contains(&stop) {
741 li.serves_mut().push(stop);
742 }
743
744 self.groups[group_idx].push_stop(stop);
745
746 self.mark_topo_dirty();
747 Ok(())
748 }
749
750 pub fn remove_stop_from_line(
756 &mut self,
757 stop: EntityId,
758 line: EntityId,
759 ) -> Result<(), SimError> {
760 let (group_idx, line_idx) = self.find_line(line)?;
761
762 self.groups[group_idx].lines_mut()[line_idx]
763 .serves_mut()
764 .retain(|&s| s != stop);
765
766 self.groups[group_idx].rebuild_caches();
768
769 self.mark_topo_dirty();
770 Ok(())
771 }
772
773 #[must_use]
777 pub fn all_lines(&self) -> Vec<EntityId> {
778 self.groups
779 .iter()
780 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
781 .collect()
782 }
783
784 #[must_use]
786 pub fn line_count(&self) -> usize {
787 self.groups.iter().map(|g| g.lines().len()).sum()
788 }
789
790 #[must_use]
792 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
793 self.groups
794 .iter()
795 .find(|g| g.id() == group)
796 .map_or_else(Vec::new, |g| {
797 g.lines().iter().map(LineInfo::entity).collect()
798 })
799 }
800
801 #[must_use]
803 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
804 self.groups
805 .iter()
806 .flat_map(ElevatorGroup::lines)
807 .find(|li| li.entity() == line)
808 .map_or_else(Vec::new, |li| li.elevators().to_vec())
809 }
810
811 #[must_use]
813 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
814 self.groups
815 .iter()
816 .flat_map(ElevatorGroup::lines)
817 .find(|li| li.entity() == line)
818 .map_or_else(Vec::new, |li| li.serves().to_vec())
819 }
820
821 #[must_use]
834 pub fn find_stop_at_position_on_line(&self, position: f64, line: EntityId) -> Option<EntityId> {
835 let line_info = self
836 .groups
837 .iter()
838 .flat_map(ElevatorGroup::lines)
839 .find(|li| li.entity() == line)?;
840 self.world
841 .find_stop_at_position_in(position, line_info.serves())
842 }
843
844 #[must_use]
846 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
847 self.groups
848 .iter()
849 .flat_map(ElevatorGroup::lines)
850 .find(|li| li.elevators().contains(&elevator))
851 .map(LineInfo::entity)
852 }
853
854 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
856 self.world
857 .iter_elevators()
858 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
859 }
860
861 #[must_use]
863 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
864 self.groups
865 .iter()
866 .flat_map(ElevatorGroup::lines)
867 .filter(|li| li.serves().contains(&stop))
868 .map(LineInfo::entity)
869 .collect()
870 }
871
872 #[must_use]
874 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
875 self.groups
876 .iter()
877 .filter(|g| g.stop_entities().contains(&stop))
878 .map(ElevatorGroup::id)
879 .collect()
880 }
881
882 fn ensure_graph_built(&self) {
886 if let Ok(mut graph) = self.topo_graph.lock()
887 && graph.is_dirty()
888 {
889 graph.rebuild(&self.groups);
890 }
891 }
892
893 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
895 self.ensure_graph_built();
896 self.topo_graph
897 .lock()
898 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
899 }
900
901 pub fn transfer_points(&self) -> Vec<EntityId> {
903 self.ensure_graph_built();
904 TopologyGraph::transfer_points(&self.groups)
905 }
906
907 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
909 self.ensure_graph_built();
910 self.topo_graph
911 .lock()
912 .ok()
913 .and_then(|g| g.shortest_route(from, to))
914 }
915}