1use crate::components::Route;
8use crate::components::{Elevator, ElevatorPhase, Line, LineKind, 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> {
230 let kind = params.kind.unwrap_or(LineKind::Linear {
235 min: params.min_position,
236 max: params.max_position,
237 });
238 kind.validate()
239 .map_err(|(field, reason)| SimError::InvalidConfig { field, reason })?;
240
241 #[cfg(feature = "loop_lines")]
253 if let LineKind::Loop {
254 circumference,
255 min_headway,
256 } = kind
257 && let Some(max_cars) = params.max_cars
258 && max_cars > 0
259 {
260 #[allow(
261 clippy::cast_precision_loss,
262 reason = "max_cars is bounded by usize; the comparison is against a finite f64"
263 )]
264 let required = (max_cars as f64) * min_headway;
265 if required > circumference {
266 return Err(SimError::InvalidConfig {
267 field: "line.kind",
268 reason: format!(
269 "loop line: {max_cars} cars × min_headway {min_headway} = {required} \
270 exceeds circumference {circumference}",
271 ),
272 });
273 }
274 }
275
276 let group_id = params.group;
277 let group = self
278 .groups
279 .iter_mut()
280 .find(|g| g.id() == group_id)
281 .ok_or(SimError::GroupNotFound(group_id))?;
282
283 let line_tag = format!("line:{}", params.name);
284
285 let eid = self.world.spawn();
286 self.world.set_line(
287 eid,
288 Line {
289 name: params.name.clone(),
290 group: group_id,
291 orientation: params.orientation,
292 position: params.position,
293 kind,
294 max_cars: params.max_cars,
295 },
296 );
297
298 group
299 .lines_mut()
300 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
301
302 if let Some(tags) = self
304 .world
305 .resource_mut::<crate::tagged_metrics::MetricTags>()
306 {
307 tags.tag(eid, line_tag);
308 }
309
310 self.mark_topo_dirty();
311 self.events.emit(Event::LineAdded {
312 line: eid,
313 group: group_id,
314 tick: self.tick,
315 });
316 Ok(eid)
317 }
318
319 pub fn set_line_range(&mut self, line: EntityId, min: f64, max: f64) -> Result<(), SimError> {
332 if !min.is_finite() || !max.is_finite() {
333 return Err(SimError::InvalidConfig {
334 field: "line.range",
335 reason: format!("min/max must be finite (got min={min}, max={max})"),
336 });
337 }
338 if min > max {
339 return Err(SimError::InvalidConfig {
340 field: "line.range",
341 reason: format!("min ({min}) must be <= max ({max})"),
342 });
343 }
344 let line_ref = self
345 .world
346 .line_mut(line)
347 .ok_or(SimError::LineNotFound(line))?;
348 match &mut line_ref.kind {
352 LineKind::Linear {
353 min: kmin,
354 max: kmax,
355 } => {
356 *kmin = min;
357 *kmax = max;
358 }
359 #[cfg(feature = "loop_lines")]
360 LineKind::Loop { .. } => {
361 return Err(SimError::InvalidConfig {
362 field: "line.range",
363 reason: "set_line_range is not valid on a Loop line; \
364 change circumference via a future API instead"
365 .to_string(),
366 });
367 }
368 }
369
370 let car_ids: Vec<EntityId> = self
372 .world
373 .iter_elevators()
374 .filter_map(|(eid, _, car)| (car.line == line).then_some(eid))
375 .collect();
376 for eid in car_ids {
377 let Some(pos) = self.world.position(eid).map(|p| p.value) else {
381 continue;
382 };
383 if pos < min || pos > max {
384 let clamped = pos.clamp(min, max);
385 if let Some(p) = self.world.position_mut(eid) {
386 p.value = clamped;
387 }
388 if let Some(v) = self.world.velocity_mut(eid) {
389 v.value = 0.0;
390 }
391 }
392 }
393
394 self.mark_topo_dirty();
395 Ok(())
396 }
397
398 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
408 let (group_idx, line_idx) = self.find_line(line)?;
409
410 let group_id = self.groups[group_idx].id();
411
412 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
414 .elevators()
415 .to_vec();
416
417 for eid in &elevator_ids {
419 let _ = self.disable(*eid);
421 }
422
423 self.groups[group_idx].lines_mut().remove(line_idx);
425
426 self.groups[group_idx].rebuild_caches();
428
429 self.world.remove_line(line);
431
432 self.mark_topo_dirty();
433 self.events.emit(Event::LineRemoved {
434 line,
435 group: group_id,
436 tick: self.tick,
437 });
438 Ok(())
439 }
440
441 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
450 let line = self
451 .world
452 .elevator(elevator)
453 .ok_or(SimError::EntityNotFound(elevator))?
454 .line();
455
456 let _ = self.disable(elevator);
458
459 let resolved_group: Option<GroupId> = match self.find_line(line) {
470 Ok((group_idx, line_idx)) => {
471 self.groups[group_idx].lines_mut()[line_idx].remove_elevator(elevator);
472 self.groups[group_idx].rebuild_caches();
473 Some(self.groups[group_idx].id())
474 }
475 Err(_) => None,
476 };
477
478 if let Some(group_id) = resolved_group {
482 self.events.emit(Event::ElevatorRemoved {
483 elevator,
484 line,
485 group: group_id,
486 tick: self.tick,
487 });
488 }
489
490 self.world.despawn(elevator);
492
493 self.mark_topo_dirty();
494 Ok(())
495 }
496
497 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
506 if self.world.stop(stop).is_none() {
507 return Err(SimError::EntityNotFound(stop));
508 }
509
510 let residents: Vec<EntityId> = self
513 .rider_index
514 .residents_at(stop)
515 .iter()
516 .copied()
517 .collect();
518 if !residents.is_empty() {
519 self.events
520 .emit(Event::ResidentsAtRemovedStop { stop, residents });
521 }
522
523 self.disable_stop_inner(stop, true);
527 self.world.disable(stop);
528 self.events.emit(Event::EntityDisabled {
529 entity: stop,
530 tick: self.tick,
531 });
532
533 let elevator_ids: Vec<EntityId> =
537 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
538 for eid in elevator_ids {
539 if let Some(car) = self.world.elevator_mut(eid) {
540 if car.target_stop == Some(stop) {
541 car.target_stop = None;
542 }
543 car.restricted_stops.remove(&stop);
544 }
545 if let Some(q) = self.world.destination_queue_mut(eid) {
546 q.retain(|s| s != stop);
547 }
548 if let Some(calls) = self.world.car_calls_mut(eid) {
553 calls.retain(|c| c.floor != stop);
554 }
555 }
556
557 for group in &mut self.groups {
559 for line_info in group.lines_mut() {
560 line_info.remove_stop(stop);
561 }
562 group.rebuild_caches();
563 }
564
565 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
567 sorted.0.retain(|&(_, s)| s != stop);
568 }
569
570 self.stop_lookup.retain(|_, &mut eid| eid != stop);
572
573 self.events.emit(Event::StopRemoved {
574 stop,
575 tick: self.tick,
576 });
577
578 self.world.despawn(stop);
580
581 self.rider_index.rebuild(&self.world);
585
586 self.mark_topo_dirty();
587 Ok(())
588 }
589
590 pub fn add_group(
592 &mut self,
593 name: impl Into<String>,
594 dispatch: impl DispatchStrategy + 'static,
595 ) -> GroupId {
596 let next_id = self
597 .groups
598 .iter()
599 .map(|g| g.id().0)
600 .max()
601 .map_or(0, |m| m + 1);
602 let group_id = GroupId(next_id);
603
604 self.groups
605 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
606
607 self.dispatcher_set
608 .insert(group_id, Box::new(dispatch), BuiltinStrategy::Scan);
609 self.mark_topo_dirty();
610 group_id
611 }
612
613 pub fn assign_line_to_group(
620 &mut self,
621 line: EntityId,
622 new_group: GroupId,
623 ) -> Result<GroupId, SimError> {
624 let (old_group_idx, line_idx) = self.find_line(line)?;
625
626 if !self.groups.iter().any(|g| g.id() == new_group) {
628 return Err(SimError::GroupNotFound(new_group));
629 }
630
631 let old_group_id = self.groups[old_group_idx].id();
632
633 if old_group_id == new_group {
638 return Ok(old_group_id);
639 }
640
641 let elevators_to_notify: Vec<EntityId> = self.groups[old_group_idx].lines()[line_idx]
646 .elevators()
647 .to_vec();
648 if let Some(dispatcher) = self.dispatcher_set.strategies_mut().get_mut(&old_group_id) {
649 for eid in &elevators_to_notify {
650 dispatcher.notify_removed(*eid);
651 }
652 }
653
654 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
656 self.groups[old_group_idx].rebuild_caches();
657
658 let new_group_idx = self
663 .groups
664 .iter()
665 .position(|g| g.id() == new_group)
666 .ok_or(SimError::GroupNotFound(new_group))?;
667 self.groups[new_group_idx].lines_mut().push(line_info);
668 self.groups[new_group_idx].rebuild_caches();
669
670 if let Some(line_comp) = self.world.line_mut(line) {
672 line_comp.group = new_group;
673 }
674
675 self.mark_topo_dirty();
676 self.events.emit(Event::LineReassigned {
677 line,
678 old_group: old_group_id,
679 new_group,
680 tick: self.tick,
681 });
682
683 Ok(old_group_id)
684 }
685
686 pub fn reassign_elevator_to_line(
697 &mut self,
698 elevator: EntityId,
699 new_line: EntityId,
700 ) -> Result<(), SimError> {
701 let old_line = self
702 .world
703 .elevator(elevator)
704 .ok_or(SimError::EntityNotFound(elevator))?
705 .line();
706
707 if old_line == new_line {
708 return Ok(());
709 }
710
711 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
713 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
714
715 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
717 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
718 .elevators()
719 .len();
720 if current_count >= max {
721 return Err(SimError::InvalidConfig {
722 field: "line.max_cars",
723 reason: format!("target line already has {current_count} cars (max {max})"),
724 });
725 }
726 }
727
728 let old_group_id = self.groups[old_group_idx].id();
729 let new_group_id = self.groups[new_group_idx].id();
730
731 self.groups[old_group_idx].lines_mut()[old_line_idx].remove_elevator(elevator);
732 self.groups[new_group_idx].lines_mut()[new_line_idx].add_elevator(elevator);
733
734 if let Some(car) = self.world.elevator_mut(elevator) {
735 car.line = new_line;
736 }
737
738 self.groups[old_group_idx].rebuild_caches();
739 if new_group_idx != old_group_idx {
740 self.groups[new_group_idx].rebuild_caches();
741
742 if let Some(old_dispatcher) =
746 self.dispatcher_set.strategies_mut().get_mut(&old_group_id)
747 {
748 old_dispatcher.notify_removed(elevator);
749 }
750 }
751
752 self.mark_topo_dirty();
753
754 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
756 elevator,
757 old_line,
758 new_line,
759 tick: self.tick,
760 });
761
762 Ok(())
763 }
764
765 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
772 if self.world.stop(stop).is_none() {
774 return Err(SimError::EntityNotFound(stop));
775 }
776
777 let (group_idx, line_idx) = self.find_line(line)?;
778
779 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
780 li.add_stop(stop);
781
782 self.groups[group_idx].push_stop(stop);
783
784 self.mark_topo_dirty();
785 Ok(())
786 }
787
788 pub fn remove_stop_from_line(
794 &mut self,
795 stop: EntityId,
796 line: EntityId,
797 ) -> Result<(), SimError> {
798 let (group_idx, line_idx) = self.find_line(line)?;
799
800 self.groups[group_idx].lines_mut()[line_idx].remove_stop(stop);
801
802 self.groups[group_idx].rebuild_caches();
804
805 self.mark_topo_dirty();
806 Ok(())
807 }
808
809 #[must_use]
813 pub fn all_lines(&self) -> Vec<EntityId> {
814 self.groups
815 .iter()
816 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
817 .collect()
818 }
819
820 #[must_use]
822 pub fn line_count(&self) -> usize {
823 self.groups.iter().map(|g| g.lines().len()).sum()
824 }
825
826 #[must_use]
828 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
829 self.groups
830 .iter()
831 .find(|g| g.id() == group)
832 .map_or_else(Vec::new, |g| {
833 g.lines().iter().map(LineInfo::entity).collect()
834 })
835 }
836
837 #[must_use]
839 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
840 self.groups
841 .iter()
842 .flat_map(ElevatorGroup::lines)
843 .find(|li| li.entity() == line)
844 .map_or_else(Vec::new, |li| li.elevators().to_vec())
845 }
846
847 #[must_use]
849 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
850 self.groups
851 .iter()
852 .flat_map(ElevatorGroup::lines)
853 .find(|li| li.entity() == line)
854 .map_or_else(Vec::new, |li| li.serves().to_vec())
855 }
856
857 #[must_use]
870 pub fn find_stop_at_position_on_line(&self, position: f64, line: EntityId) -> Option<EntityId> {
871 let line_info = self
872 .groups
873 .iter()
874 .flat_map(ElevatorGroup::lines)
875 .find(|li| li.entity() == line)?;
876 self.world
877 .find_stop_at_position_in(position, line_info.serves())
878 }
879
880 #[must_use]
882 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
883 self.groups
884 .iter()
885 .flat_map(ElevatorGroup::lines)
886 .find(|li| li.elevators().contains(&elevator))
887 .map(LineInfo::entity)
888 }
889
890 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
892 self.world
893 .iter_elevators()
894 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
895 }
896
897 #[must_use]
899 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
900 self.groups
901 .iter()
902 .flat_map(ElevatorGroup::lines)
903 .filter(|li| li.serves().contains(&stop))
904 .map(LineInfo::entity)
905 .collect()
906 }
907
908 #[must_use]
910 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
911 self.groups
912 .iter()
913 .filter(|g| g.stop_entities().contains(&stop))
914 .map(ElevatorGroup::id)
915 .collect()
916 }
917
918 fn ensure_graph_built(&self) {
922 if let Ok(mut graph) = self.topo_graph.lock()
923 && graph.is_dirty()
924 {
925 graph.rebuild(&self.groups);
926 }
927 }
928
929 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
931 self.ensure_graph_built();
932 self.topo_graph
933 .lock()
934 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
935 }
936
937 pub fn transfer_points(&self) -> Vec<EntityId> {
939 self.ensure_graph_built();
940 TopologyGraph::transfer_points(&self.groups)
941 }
942
943 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
945 self.ensure_graph_built();
946 self.topo_graph
947 .lock()
948 .ok()
949 .and_then(|g| g.shortest_route(from, to))
950 }
951}