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: 0.0,
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 },
164 );
165 self.world
166 .set_destination_queue(eid, crate::components::DestinationQueue::new());
167 self.groups[group_idx].lines_mut()[line_idx]
168 .elevators_mut()
169 .push(eid);
170 self.groups[group_idx].push_elevator(eid);
171
172 let line_name = self.world.line(line).map(|l| l.name.clone());
174 if let Some(name) = line_name
175 && let Some(tags) = self
176 .world
177 .resource_mut::<crate::tagged_metrics::MetricTags>()
178 {
179 tags.tag(eid, format!("line:{name}"));
180 }
181
182 self.mark_topo_dirty();
183 self.events.emit(Event::ElevatorAdded {
184 elevator: eid,
185 line,
186 group: group_id,
187 tick: self.tick,
188 });
189 Ok(eid)
190 }
191
192 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
200 let group_id = params.group;
201 let group = self
202 .groups
203 .iter_mut()
204 .find(|g| g.id() == group_id)
205 .ok_or(SimError::GroupNotFound(group_id))?;
206
207 let line_tag = format!("line:{}", params.name);
208
209 let eid = self.world.spawn();
210 self.world.set_line(
211 eid,
212 Line {
213 name: params.name.clone(),
214 group: group_id,
215 orientation: params.orientation,
216 position: params.position,
217 min_position: params.min_position,
218 max_position: params.max_position,
219 max_cars: params.max_cars,
220 },
221 );
222
223 group
224 .lines_mut()
225 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
226
227 if let Some(tags) = self
229 .world
230 .resource_mut::<crate::tagged_metrics::MetricTags>()
231 {
232 tags.tag(eid, line_tag);
233 }
234
235 self.mark_topo_dirty();
236 self.events.emit(Event::LineAdded {
237 line: eid,
238 group: group_id,
239 tick: self.tick,
240 });
241 Ok(eid)
242 }
243
244 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
254 let (group_idx, line_idx) = self.find_line(line)?;
255
256 let group_id = self.groups[group_idx].id();
257
258 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
260 .elevators()
261 .to_vec();
262
263 for eid in &elevator_ids {
265 let _ = self.disable(*eid);
267 }
268
269 self.groups[group_idx].lines_mut().remove(line_idx);
271
272 self.groups[group_idx].rebuild_caches();
274
275 self.world.remove_line(line);
277
278 self.mark_topo_dirty();
279 self.events.emit(Event::LineRemoved {
280 line,
281 group: group_id,
282 tick: self.tick,
283 });
284 Ok(())
285 }
286
287 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
296 let line = self
297 .world
298 .elevator(elevator)
299 .ok_or(SimError::EntityNotFound(elevator))?
300 .line();
301
302 let _ = self.disable(elevator);
304
305 let mut group_id = GroupId(0);
307 if let Ok((group_idx, line_idx)) = self.find_line(line) {
308 self.groups[group_idx].lines_mut()[line_idx]
309 .elevators_mut()
310 .retain(|&e| e != elevator);
311 self.groups[group_idx].rebuild_caches();
312
313 group_id = self.groups[group_idx].id();
315 if let Some(dispatcher) = self.dispatchers.get_mut(&group_id) {
316 dispatcher.notify_removed(elevator);
317 }
318 }
319
320 self.events.emit(Event::ElevatorRemoved {
321 elevator,
322 line,
323 group: group_id,
324 tick: self.tick,
325 });
326
327 self.world.despawn(elevator);
329
330 self.mark_topo_dirty();
331 Ok(())
332 }
333
334 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
343 if self.world.stop(stop).is_none() {
344 return Err(SimError::EntityNotFound(stop));
345 }
346
347 let _ = self.disable(stop);
349
350 let elevator_ids: Vec<EntityId> =
354 self.world.iter_elevators().map(|(eid, _, _)| eid).collect();
355 for eid in elevator_ids {
356 if let Some(car) = self.world.elevator_mut(eid) {
357 if car.target_stop == Some(stop) {
358 car.target_stop = None;
359 }
360 car.restricted_stops.remove(&stop);
361 }
362 if let Some(q) = self.world.destination_queue_mut(eid) {
363 q.retain(|s| s != stop);
364 }
365 }
366
367 for group in &mut self.groups {
369 for line_info in group.lines_mut() {
370 line_info.serves_mut().retain(|&s| s != stop);
371 }
372 group.rebuild_caches();
373 }
374
375 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
377 sorted.0.retain(|&(_, s)| s != stop);
378 }
379
380 self.stop_lookup.retain(|_, &mut eid| eid != stop);
382
383 self.events.emit(Event::StopRemoved {
384 stop,
385 tick: self.tick,
386 });
387
388 self.world.despawn(stop);
390
391 self.mark_topo_dirty();
392 Ok(())
393 }
394
395 pub fn add_group(
397 &mut self,
398 name: impl Into<String>,
399 dispatch: impl DispatchStrategy + 'static,
400 ) -> GroupId {
401 let next_id = self
402 .groups
403 .iter()
404 .map(|g| g.id().0)
405 .max()
406 .map_or(0, |m| m + 1);
407 let group_id = GroupId(next_id);
408
409 self.groups
410 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
411
412 self.dispatchers.insert(group_id, Box::new(dispatch));
413 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
414 self.mark_topo_dirty();
415 group_id
416 }
417
418 pub fn assign_line_to_group(
425 &mut self,
426 line: EntityId,
427 new_group: GroupId,
428 ) -> Result<GroupId, SimError> {
429 let (old_group_idx, line_idx) = self.find_line(line)?;
430
431 if !self.groups.iter().any(|g| g.id() == new_group) {
433 return Err(SimError::GroupNotFound(new_group));
434 }
435
436 let old_group_id = self.groups[old_group_idx].id();
437
438 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
440 self.groups[old_group_idx].rebuild_caches();
441
442 let new_group_idx = self
447 .groups
448 .iter()
449 .position(|g| g.id() == new_group)
450 .ok_or(SimError::GroupNotFound(new_group))?;
451 self.groups[new_group_idx].lines_mut().push(line_info);
452 self.groups[new_group_idx].rebuild_caches();
453
454 if let Some(line_comp) = self.world.line_mut(line) {
456 line_comp.group = new_group;
457 }
458
459 self.mark_topo_dirty();
460 self.events.emit(Event::LineReassigned {
461 line,
462 old_group: old_group_id,
463 new_group,
464 tick: self.tick,
465 });
466
467 Ok(old_group_id)
468 }
469
470 pub fn reassign_elevator_to_line(
481 &mut self,
482 elevator: EntityId,
483 new_line: EntityId,
484 ) -> Result<(), SimError> {
485 let old_line = self
486 .world
487 .elevator(elevator)
488 .ok_or(SimError::EntityNotFound(elevator))?
489 .line();
490
491 if old_line == new_line {
492 return Ok(());
493 }
494
495 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
497 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
498
499 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
501 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
502 .elevators()
503 .len();
504 if current_count >= max {
505 return Err(SimError::InvalidConfig {
506 field: "line.max_cars",
507 reason: format!("target line already has {current_count} cars (max {max})"),
508 });
509 }
510 }
511
512 let old_group_id = self.groups[old_group_idx].id();
513 let new_group_id = self.groups[new_group_idx].id();
514
515 self.groups[old_group_idx].lines_mut()[old_line_idx]
516 .elevators_mut()
517 .retain(|&e| e != elevator);
518 self.groups[new_group_idx].lines_mut()[new_line_idx]
519 .elevators_mut()
520 .push(elevator);
521
522 if let Some(car) = self.world.elevator_mut(elevator) {
523 car.line = new_line;
524 }
525
526 self.groups[old_group_idx].rebuild_caches();
527 if new_group_idx != old_group_idx {
528 self.groups[new_group_idx].rebuild_caches();
529
530 if let Some(old_dispatcher) = self.dispatchers.get_mut(&old_group_id) {
534 old_dispatcher.notify_removed(elevator);
535 }
536 }
537
538 self.mark_topo_dirty();
539
540 let _ = new_group_id; self.events.emit(Event::ElevatorReassigned {
542 elevator,
543 old_line,
544 new_line,
545 tick: self.tick,
546 });
547
548 Ok(())
549 }
550
551 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
558 if self.world.stop(stop).is_none() {
560 return Err(SimError::EntityNotFound(stop));
561 }
562
563 let (group_idx, line_idx) = self.find_line(line)?;
564
565 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
566 if !li.serves().contains(&stop) {
567 li.serves_mut().push(stop);
568 }
569
570 self.groups[group_idx].push_stop(stop);
571
572 self.mark_topo_dirty();
573 Ok(())
574 }
575
576 pub fn remove_stop_from_line(
582 &mut self,
583 stop: EntityId,
584 line: EntityId,
585 ) -> Result<(), SimError> {
586 let (group_idx, line_idx) = self.find_line(line)?;
587
588 self.groups[group_idx].lines_mut()[line_idx]
589 .serves_mut()
590 .retain(|&s| s != stop);
591
592 self.groups[group_idx].rebuild_caches();
594
595 self.mark_topo_dirty();
596 Ok(())
597 }
598
599 #[must_use]
603 pub fn all_lines(&self) -> Vec<EntityId> {
604 self.groups
605 .iter()
606 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
607 .collect()
608 }
609
610 #[must_use]
612 pub fn line_count(&self) -> usize {
613 self.groups.iter().map(|g| g.lines().len()).sum()
614 }
615
616 #[must_use]
618 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
619 self.groups
620 .iter()
621 .find(|g| g.id() == group)
622 .map_or_else(Vec::new, |g| {
623 g.lines().iter().map(LineInfo::entity).collect()
624 })
625 }
626
627 #[must_use]
629 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
630 self.groups
631 .iter()
632 .flat_map(ElevatorGroup::lines)
633 .find(|li| li.entity() == line)
634 .map_or_else(Vec::new, |li| li.elevators().to_vec())
635 }
636
637 #[must_use]
639 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
640 self.groups
641 .iter()
642 .flat_map(ElevatorGroup::lines)
643 .find(|li| li.entity() == line)
644 .map_or_else(Vec::new, |li| li.serves().to_vec())
645 }
646
647 #[must_use]
649 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
650 self.groups
651 .iter()
652 .flat_map(ElevatorGroup::lines)
653 .find(|li| li.elevators().contains(&elevator))
654 .map(LineInfo::entity)
655 }
656
657 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
659 self.world
660 .iter_elevators()
661 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
662 }
663
664 #[must_use]
666 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
667 self.groups
668 .iter()
669 .flat_map(ElevatorGroup::lines)
670 .filter(|li| li.serves().contains(&stop))
671 .map(LineInfo::entity)
672 .collect()
673 }
674
675 #[must_use]
677 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
678 self.groups
679 .iter()
680 .filter(|g| g.stop_entities().contains(&stop))
681 .map(ElevatorGroup::id)
682 .collect()
683 }
684
685 pub(super) fn ensure_graph_built(&self) {
689 if let Ok(mut graph) = self.topo_graph.lock()
690 && graph.is_dirty()
691 {
692 graph.rebuild(&self.groups);
693 }
694 }
695
696 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
698 self.ensure_graph_built();
699 self.topo_graph
700 .lock()
701 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
702 }
703
704 pub fn transfer_points(&self) -> Vec<EntityId> {
706 self.ensure_graph_built();
707 TopologyGraph::transfer_points(&self.groups)
708 }
709
710 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
712 self.ensure_graph_built();
713 self.topo_graph
714 .lock()
715 .ok()
716 .and_then(|g| g.shortest_route(from, to))
717 }
718}