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 pub(super) fn mark_topo_dirty(&self) {
24 if let Ok(mut g) = self.topo_graph.lock() {
25 g.mark_dirty();
26 }
27 }
28
29 pub(super) 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 let group_id = self
116 .world
117 .line(line)
118 .map(|l| l.group)
119 .ok_or(SimError::LineNotFound(line))?;
120
121 let (group_idx, line_idx) = self.find_line(line)?;
122
123 if let Some(max) = self.world.line(line).and_then(Line::max_cars) {
125 let current_count = self.groups[group_idx].lines()[line_idx].elevators().len();
126 if current_count >= max {
127 return Err(SimError::InvalidConfig {
128 field: "line.max_cars",
129 reason: format!("line already has {current_count} cars (max {max})"),
130 });
131 }
132 }
133
134 let eid = self.world.spawn();
135 self.world.set_position(
136 eid,
137 Position {
138 value: starting_position,
139 },
140 );
141 self.world.set_velocity(eid, Velocity { value: 0.0 });
142 self.world.set_elevator(
143 eid,
144 Elevator {
145 phase: ElevatorPhase::Idle,
146 door: DoorState::Closed,
147 max_speed: params.max_speed,
148 acceleration: params.acceleration,
149 deceleration: params.deceleration,
150 weight_capacity: params.weight_capacity,
151 current_load: crate::components::Weight::ZERO,
152 riders: Vec::new(),
153 target_stop: None,
154 door_transition_ticks: params.door_transition_ticks,
155 door_open_ticks: params.door_open_ticks,
156 line,
157 repositioning: false,
158 restricted_stops: params.restricted_stops.clone(),
159 inspection_speed_factor: params.inspection_speed_factor,
160 going_up: true,
161 going_down: true,
162 move_count: 0,
163 door_command_queue: Vec::new(),
164 manual_target_velocity: None,
165 },
166 );
167 self.world
168 .set_destination_queue(eid, crate::components::DestinationQueue::new());
169 self.groups[group_idx].lines_mut()[line_idx]
170 .elevators_mut()
171 .push(eid);
172 self.groups[group_idx].push_elevator(eid);
173
174 let line_name = self.world.line(line).map(|l| l.name.clone());
176 if let Some(name) = line_name
177 && let Some(tags) = self
178 .world
179 .resource_mut::<crate::tagged_metrics::MetricTags>()
180 {
181 tags.tag(eid, format!("line:{name}"));
182 }
183
184 self.mark_topo_dirty();
185 self.events.emit(Event::ElevatorAdded {
186 elevator: eid,
187 line,
188 group: group_id,
189 tick: self.tick,
190 });
191 Ok(eid)
192 }
193
194 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
202 let group_id = params.group;
203 let group = self
204 .groups
205 .iter_mut()
206 .find(|g| g.id() == group_id)
207 .ok_or(SimError::GroupNotFound(group_id))?;
208
209 let line_tag = format!("line:{}", params.name);
210
211 let eid = self.world.spawn();
212 self.world.set_line(
213 eid,
214 Line {
215 name: params.name.clone(),
216 group: group_id,
217 orientation: params.orientation,
218 position: params.position,
219 min_position: params.min_position,
220 max_position: params.max_position,
221 max_cars: params.max_cars,
222 },
223 );
224
225 group
226 .lines_mut()
227 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
228
229 if let Some(tags) = self
231 .world
232 .resource_mut::<crate::tagged_metrics::MetricTags>()
233 {
234 tags.tag(eid, line_tag);
235 }
236
237 self.mark_topo_dirty();
238 self.events.emit(Event::LineAdded {
239 line: eid,
240 group: group_id,
241 tick: self.tick,
242 });
243 Ok(eid)
244 }
245
246 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
256 let (group_idx, line_idx) = self.find_line(line)?;
257
258 let group_id = self.groups[group_idx].id();
259
260 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
262 .elevators()
263 .to_vec();
264
265 for eid in &elevator_ids {
267 let _ = self.disable(*eid);
269 }
270
271 self.groups[group_idx].lines_mut().remove(line_idx);
273
274 self.groups[group_idx].rebuild_caches();
276
277 self.world.remove_line(line);
279
280 self.mark_topo_dirty();
281 self.events.emit(Event::LineRemoved {
282 line,
283 group: group_id,
284 tick: self.tick,
285 });
286 Ok(())
287 }
288
289 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
298 let line = self
299 .world
300 .elevator(elevator)
301 .ok_or(SimError::EntityNotFound(elevator))?
302 .line();
303
304 let _ = self.disable(elevator);
306
307 let mut group_id = GroupId(0);
309 if let Ok((group_idx, line_idx)) = self.find_line(line) {
310 self.groups[group_idx].lines_mut()[line_idx]
311 .elevators_mut()
312 .retain(|&e| e != elevator);
313 self.groups[group_idx].rebuild_caches();
314
315 group_id = self.groups[group_idx].id();
317 if let Some(dispatcher) = self.dispatchers.get_mut(&group_id) {
318 dispatcher.notify_removed(elevator);
319 }
320 }
321
322 self.events.emit(Event::ElevatorRemoved {
323 elevator,
324 line,
325 group: group_id,
326 tick: self.tick,
327 });
328
329 self.world.despawn(elevator);
331
332 self.mark_topo_dirty();
333 Ok(())
334 }
335
336 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
345 if self.world.stop(stop).is_none() {
346 return Err(SimError::EntityNotFound(stop));
347 }
348
349 let residents: Vec<EntityId> = self
352 .rider_index
353 .residents_at(stop)
354 .iter()
355 .copied()
356 .collect();
357 if !residents.is_empty() {
358 self.events
359 .emit(Event::ResidentsAtRemovedStop { stop, residents });
360 }
361
362 let _ = self.disable(stop);
364
365 let elevator_ids: Vec<EntityId> =
369 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
370 for eid in elevator_ids {
371 if let Some(car) = self.world.elevator_mut(eid) {
372 if car.target_stop == Some(stop) {
373 car.target_stop = None;
374 }
375 car.restricted_stops.remove(&stop);
376 }
377 if let Some(q) = self.world.destination_queue_mut(eid) {
378 q.retain(|s| s != stop);
379 }
380 }
381
382 for group in &mut self.groups {
384 for line_info in group.lines_mut() {
385 line_info.serves_mut().retain(|&s| s != stop);
386 }
387 group.rebuild_caches();
388 }
389
390 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
392 sorted.0.retain(|&(_, s)| s != stop);
393 }
394
395 self.stop_lookup.retain(|_, &mut eid| eid != stop);
397
398 self.events.emit(Event::StopRemoved {
399 stop,
400 tick: self.tick,
401 });
402
403 self.world.despawn(stop);
405
406 self.mark_topo_dirty();
407 Ok(())
408 }
409
410 pub fn add_group(
412 &mut self,
413 name: impl Into<String>,
414 dispatch: impl DispatchStrategy + 'static,
415 ) -> GroupId {
416 let next_id = self
417 .groups
418 .iter()
419 .map(|g| g.id().0)
420 .max()
421 .map_or(0, |m| m + 1);
422 let group_id = GroupId(next_id);
423
424 self.groups
425 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
426
427 self.dispatchers.insert(group_id, Box::new(dispatch));
428 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
429 self.mark_topo_dirty();
430 group_id
431 }
432
433 pub fn assign_line_to_group(
440 &mut self,
441 line: EntityId,
442 new_group: GroupId,
443 ) -> Result<GroupId, SimError> {
444 let (old_group_idx, line_idx) = self.find_line(line)?;
445
446 if !self.groups.iter().any(|g| g.id() == new_group) {
448 return Err(SimError::GroupNotFound(new_group));
449 }
450
451 let old_group_id = self.groups[old_group_idx].id();
452
453 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
455 self.groups[old_group_idx].rebuild_caches();
456
457 let new_group_idx = self
462 .groups
463 .iter()
464 .position(|g| g.id() == new_group)
465 .ok_or(SimError::GroupNotFound(new_group))?;
466 self.groups[new_group_idx].lines_mut().push(line_info);
467 self.groups[new_group_idx].rebuild_caches();
468
469 if let Some(line_comp) = self.world.line_mut(line) {
471 line_comp.group = new_group;
472 }
473
474 self.mark_topo_dirty();
475 self.events.emit(Event::LineReassigned {
476 line,
477 old_group: old_group_id,
478 new_group,
479 tick: self.tick,
480 });
481
482 Ok(old_group_id)
483 }
484
485 pub fn reassign_elevator_to_line(
496 &mut self,
497 elevator: EntityId,
498 new_line: EntityId,
499 ) -> Result<(), SimError> {
500 let old_line = self
501 .world
502 .elevator(elevator)
503 .ok_or(SimError::EntityNotFound(elevator))?
504 .line();
505
506 if old_line == new_line {
507 return Ok(());
508 }
509
510 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
512 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
513
514 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
516 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
517 .elevators()
518 .len();
519 if current_count >= max {
520 return Err(SimError::InvalidConfig {
521 field: "line.max_cars",
522 reason: format!("target line already has {current_count} cars (max {max})"),
523 });
524 }
525 }
526
527 let old_group_id = self.groups[old_group_idx].id();
528 let new_group_id = self.groups[new_group_idx].id();
529
530 self.groups[old_group_idx].lines_mut()[old_line_idx]
531 .elevators_mut()
532 .retain(|&e| e != elevator);
533 self.groups[new_group_idx].lines_mut()[new_line_idx]
534 .elevators_mut()
535 .push(elevator);
536
537 if let Some(car) = self.world.elevator_mut(elevator) {
538 car.line = new_line;
539 }
540
541 self.groups[old_group_idx].rebuild_caches();
542 if new_group_idx != old_group_idx {
543 self.groups[new_group_idx].rebuild_caches();
544
545 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
549 old_dispatcher.notify_removed(elevator);
550 }
551 }
552
553 self.mark_topo_dirty();
554
555 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
557 elevator,
558 old_line,
559 new_line,
560 tick: self.tick,
561 });
562
563 Ok(())
564 }
565
566 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
573 if self.world.stop(stop).is_none() {
575 return Err(SimError::EntityNotFound(stop));
576 }
577
578 let (group_idx, line_idx) = self.find_line(line)?;
579
580 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
581 if !li.serves().contains(&stop) {
582 li.serves_mut().push(stop);
583 }
584
585 self.groups[group_idx].push_stop(stop);
586
587 self.mark_topo_dirty();
588 Ok(())
589 }
590
591 pub fn remove_stop_from_line(
597 &mut self,
598 stop: EntityId,
599 line: EntityId,
600 ) -> Result<(), SimError> {
601 let (group_idx, line_idx) = self.find_line(line)?;
602
603 self.groups[group_idx].lines_mut()[line_idx]
604 .serves_mut()
605 .retain(|&s| s != stop);
606
607 self.groups[group_idx].rebuild_caches();
609
610 self.mark_topo_dirty();
611 Ok(())
612 }
613
614 #[must_use]
618 pub fn all_lines(&self) -> Vec<EntityId> {
619 self.groups
620 .iter()
621 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
622 .collect()
623 }
624
625 #[must_use]
627 pub fn line_count(&self) -> usize {
628 self.groups.iter().map(|g| g.lines().len()).sum()
629 }
630
631 #[must_use]
633 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
634 self.groups
635 .iter()
636 .find(|g| g.id() == group)
637 .map_or_else(Vec::new, |g| {
638 g.lines().iter().map(LineInfo::entity).collect()
639 })
640 }
641
642 #[must_use]
644 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
645 self.groups
646 .iter()
647 .flat_map(ElevatorGroup::lines)
648 .find(|li| li.entity() == line)
649 .map_or_else(Vec::new, |li| li.elevators().to_vec())
650 }
651
652 #[must_use]
654 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
655 self.groups
656 .iter()
657 .flat_map(ElevatorGroup::lines)
658 .find(|li| li.entity() == line)
659 .map_or_else(Vec::new, |li| li.serves().to_vec())
660 }
661
662 #[must_use]
664 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
665 self.groups
666 .iter()
667 .flat_map(ElevatorGroup::lines)
668 .find(|li| li.elevators().contains(&elevator))
669 .map(LineInfo::entity)
670 }
671
672 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
674 self.world
675 .iter_elevators()
676 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
677 }
678
679 #[must_use]
681 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
682 self.groups
683 .iter()
684 .flat_map(ElevatorGroup::lines)
685 .filter(|li| li.serves().contains(&stop))
686 .map(LineInfo::entity)
687 .collect()
688 }
689
690 #[must_use]
692 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
693 self.groups
694 .iter()
695 .filter(|g| g.stop_entities().contains(&stop))
696 .map(ElevatorGroup::id)
697 .collect()
698 }
699
700 pub(super) fn ensure_graph_built(&self) {
704 if let Ok(mut graph) = self.topo_graph.lock()
705 && graph.is_dirty()
706 {
707 graph.rebuild(&self.groups);
708 }
709 }
710
711 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
713 self.ensure_graph_built();
714 self.topo_graph
715 .lock()
716 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
717 }
718
719 pub fn transfer_points(&self) -> Vec<EntityId> {
721 self.ensure_graph_built();
722 TopologyGraph::transfer_points(&self.groups)
723 }
724
725 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
727 self.ensure_graph_built();
728 self.topo_graph
729 .lock()
730 .ok()
731 .and_then(|g| g.shortest_route(from, to))
732 }
733}