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 )?;
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 },
187 );
188 self.world
189 .set_destination_queue(eid, crate::components::DestinationQueue::new());
190 self.groups[group_idx].lines_mut()[line_idx]
191 .elevators_mut()
192 .push(eid);
193 self.groups[group_idx].push_elevator(eid);
194
195 let line_name = self.world.line(line).map(|l| l.name.clone());
197 if let Some(name) = line_name
198 && let Some(tags) = self
199 .world
200 .resource_mut::<crate::tagged_metrics::MetricTags>()
201 {
202 tags.tag(eid, format!("line:{name}"));
203 }
204
205 self.mark_topo_dirty();
206 self.events.emit(Event::ElevatorAdded {
207 elevator: eid,
208 line,
209 group: group_id,
210 tick: self.tick,
211 });
212 Ok(eid)
213 }
214
215 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
223 let group_id = params.group;
224 let group = self
225 .groups
226 .iter_mut()
227 .find(|g| g.id() == group_id)
228 .ok_or(SimError::GroupNotFound(group_id))?;
229
230 let line_tag = format!("line:{}", params.name);
231
232 let eid = self.world.spawn();
233 self.world.set_line(
234 eid,
235 Line {
236 name: params.name.clone(),
237 group: group_id,
238 orientation: params.orientation,
239 position: params.position,
240 min_position: params.min_position,
241 max_position: params.max_position,
242 max_cars: params.max_cars,
243 },
244 );
245
246 group
247 .lines_mut()
248 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
249
250 if let Some(tags) = self
252 .world
253 .resource_mut::<crate::tagged_metrics::MetricTags>()
254 {
255 tags.tag(eid, line_tag);
256 }
257
258 self.mark_topo_dirty();
259 self.events.emit(Event::LineAdded {
260 line: eid,
261 group: group_id,
262 tick: self.tick,
263 });
264 Ok(eid)
265 }
266
267 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
277 let (group_idx, line_idx) = self.find_line(line)?;
278
279 let group_id = self.groups[group_idx].id();
280
281 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
283 .elevators()
284 .to_vec();
285
286 for eid in &elevator_ids {
288 let _ = self.disable(*eid);
290 }
291
292 self.groups[group_idx].lines_mut().remove(line_idx);
294
295 self.groups[group_idx].rebuild_caches();
297
298 self.world.remove_line(line);
300
301 self.mark_topo_dirty();
302 self.events.emit(Event::LineRemoved {
303 line,
304 group: group_id,
305 tick: self.tick,
306 });
307 Ok(())
308 }
309
310 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
319 let line = self
320 .world
321 .elevator(elevator)
322 .ok_or(SimError::EntityNotFound(elevator))?
323 .line();
324
325 let _ = self.disable(elevator);
327
328 let mut group_id = GroupId(0);
330 if let Ok((group_idx, line_idx)) = self.find_line(line) {
331 self.groups[group_idx].lines_mut()[line_idx]
332 .elevators_mut()
333 .retain(|&e| e != elevator);
334 self.groups[group_idx].rebuild_caches();
335
336 group_id = self.groups[group_idx].id();
338 if let Some(dispatcher) = self.dispatchers.get_mut(&group_id) {
339 dispatcher.notify_removed(elevator);
340 }
341 }
342
343 self.events.emit(Event::ElevatorRemoved {
344 elevator,
345 line,
346 group: group_id,
347 tick: self.tick,
348 });
349
350 self.world.despawn(elevator);
352
353 self.mark_topo_dirty();
354 Ok(())
355 }
356
357 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
366 if self.world.stop(stop).is_none() {
367 return Err(SimError::EntityNotFound(stop));
368 }
369
370 let residents: Vec<EntityId> = self
373 .rider_index
374 .residents_at(stop)
375 .iter()
376 .copied()
377 .collect();
378 if !residents.is_empty() {
379 self.events
380 .emit(Event::ResidentsAtRemovedStop { stop, residents });
381 }
382
383 let _ = self.disable(stop);
385
386 let elevator_ids: Vec<EntityId> =
390 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
391 for eid in elevator_ids {
392 if let Some(car) = self.world.elevator_mut(eid) {
393 if car.target_stop == Some(stop) {
394 car.target_stop = None;
395 }
396 car.restricted_stops.remove(&stop);
397 }
398 if let Some(q) = self.world.destination_queue_mut(eid) {
399 q.retain(|s| s != stop);
400 }
401 }
402
403 for group in &mut self.groups {
405 for line_info in group.lines_mut() {
406 line_info.serves_mut().retain(|&s| s != stop);
407 }
408 group.rebuild_caches();
409 }
410
411 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
413 sorted.0.retain(|&(_, s)| s != stop);
414 }
415
416 self.stop_lookup.retain(|_, &mut eid| eid != stop);
418
419 self.events.emit(Event::StopRemoved {
420 stop,
421 tick: self.tick,
422 });
423
424 self.world.despawn(stop);
426
427 self.mark_topo_dirty();
428 Ok(())
429 }
430
431 pub fn add_group(
433 &mut self,
434 name: impl Into<String>,
435 dispatch: impl DispatchStrategy + 'static,
436 ) -> GroupId {
437 let next_id = self
438 .groups
439 .iter()
440 .map(|g| g.id().0)
441 .max()
442 .map_or(0, |m| m + 1);
443 let group_id = GroupId(next_id);
444
445 self.groups
446 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
447
448 self.dispatchers.insert(group_id, Box::new(dispatch));
449 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
450 self.mark_topo_dirty();
451 group_id
452 }
453
454 pub fn assign_line_to_group(
461 &mut self,
462 line: EntityId,
463 new_group: GroupId,
464 ) -> Result<GroupId, SimError> {
465 let (old_group_idx, line_idx) = self.find_line(line)?;
466
467 if !self.groups.iter().any(|g| g.id() == new_group) {
469 return Err(SimError::GroupNotFound(new_group));
470 }
471
472 let old_group_id = self.groups[old_group_idx].id();
473
474 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
476 self.groups[old_group_idx].rebuild_caches();
477
478 let new_group_idx = self
483 .groups
484 .iter()
485 .position(|g| g.id() == new_group)
486 .ok_or(SimError::GroupNotFound(new_group))?;
487 self.groups[new_group_idx].lines_mut().push(line_info);
488 self.groups[new_group_idx].rebuild_caches();
489
490 if let Some(line_comp) = self.world.line_mut(line) {
492 line_comp.group = new_group;
493 }
494
495 self.mark_topo_dirty();
496 self.events.emit(Event::LineReassigned {
497 line,
498 old_group: old_group_id,
499 new_group,
500 tick: self.tick,
501 });
502
503 Ok(old_group_id)
504 }
505
506 pub fn reassign_elevator_to_line(
517 &mut self,
518 elevator: EntityId,
519 new_line: EntityId,
520 ) -> Result<(), SimError> {
521 let old_line = self
522 .world
523 .elevator(elevator)
524 .ok_or(SimError::EntityNotFound(elevator))?
525 .line();
526
527 if old_line == new_line {
528 return Ok(());
529 }
530
531 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
533 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
534
535 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
537 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
538 .elevators()
539 .len();
540 if current_count >= max {
541 return Err(SimError::InvalidConfig {
542 field: "line.max_cars",
543 reason: format!("target line already has {current_count} cars (max {max})"),
544 });
545 }
546 }
547
548 let old_group_id = self.groups[old_group_idx].id();
549 let new_group_id = self.groups[new_group_idx].id();
550
551 self.groups[old_group_idx].lines_mut()[old_line_idx]
552 .elevators_mut()
553 .retain(|&e| e != elevator);
554 self.groups[new_group_idx].lines_mut()[new_line_idx]
555 .elevators_mut()
556 .push(elevator);
557
558 if let Some(car) = self.world.elevator_mut(elevator) {
559 car.line = new_line;
560 }
561
562 self.groups[old_group_idx].rebuild_caches();
563 if new_group_idx != old_group_idx {
564 self.groups[new_group_idx].rebuild_caches();
565
566 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
570 old_dispatcher.notify_removed(elevator);
571 }
572 }
573
574 self.mark_topo_dirty();
575
576 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
578 elevator,
579 old_line,
580 new_line,
581 tick: self.tick,
582 });
583
584 Ok(())
585 }
586
587 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
594 if self.world.stop(stop).is_none() {
596 return Err(SimError::EntityNotFound(stop));
597 }
598
599 let (group_idx, line_idx) = self.find_line(line)?;
600
601 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
602 if !li.serves().contains(&stop) {
603 li.serves_mut().push(stop);
604 }
605
606 self.groups[group_idx].push_stop(stop);
607
608 self.mark_topo_dirty();
609 Ok(())
610 }
611
612 pub fn remove_stop_from_line(
618 &mut self,
619 stop: EntityId,
620 line: EntityId,
621 ) -> Result<(), SimError> {
622 let (group_idx, line_idx) = self.find_line(line)?;
623
624 self.groups[group_idx].lines_mut()[line_idx]
625 .serves_mut()
626 .retain(|&s| s != stop);
627
628 self.groups[group_idx].rebuild_caches();
630
631 self.mark_topo_dirty();
632 Ok(())
633 }
634
635 #[must_use]
639 pub fn all_lines(&self) -> Vec<EntityId> {
640 self.groups
641 .iter()
642 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
643 .collect()
644 }
645
646 #[must_use]
648 pub fn line_count(&self) -> usize {
649 self.groups.iter().map(|g| g.lines().len()).sum()
650 }
651
652 #[must_use]
654 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
655 self.groups
656 .iter()
657 .find(|g| g.id() == group)
658 .map_or_else(Vec::new, |g| {
659 g.lines().iter().map(LineInfo::entity).collect()
660 })
661 }
662
663 #[must_use]
665 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
666 self.groups
667 .iter()
668 .flat_map(ElevatorGroup::lines)
669 .find(|li| li.entity() == line)
670 .map_or_else(Vec::new, |li| li.elevators().to_vec())
671 }
672
673 #[must_use]
675 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
676 self.groups
677 .iter()
678 .flat_map(ElevatorGroup::lines)
679 .find(|li| li.entity() == line)
680 .map_or_else(Vec::new, |li| li.serves().to_vec())
681 }
682
683 #[must_use]
685 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
686 self.groups
687 .iter()
688 .flat_map(ElevatorGroup::lines)
689 .find(|li| li.elevators().contains(&elevator))
690 .map(LineInfo::entity)
691 }
692
693 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
695 self.world
696 .iter_elevators()
697 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
698 }
699
700 #[must_use]
702 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
703 self.groups
704 .iter()
705 .flat_map(ElevatorGroup::lines)
706 .filter(|li| li.serves().contains(&stop))
707 .map(LineInfo::entity)
708 .collect()
709 }
710
711 #[must_use]
713 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
714 self.groups
715 .iter()
716 .filter(|g| g.stop_entities().contains(&stop))
717 .map(ElevatorGroup::id)
718 .collect()
719 }
720
721 fn ensure_graph_built(&self) {
725 if let Ok(mut graph) = self.topo_graph.lock()
726 && graph.is_dirty()
727 {
728 graph.rebuild(&self.groups);
729 }
730 }
731
732 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
734 self.ensure_graph_built();
735 self.topo_graph
736 .lock()
737 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
738 }
739
740 pub fn transfer_points(&self) -> Vec<EntityId> {
742 self.ensure_graph_built();
743 TopologyGraph::transfer_points(&self.groups)
744 }
745
746 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
748 self.ensure_graph_built();
749 self.topo_graph
750 .lock()
751 .ok()
752 .and_then(|g| g.shortest_route(from, to))
753 }
754}