1use std::ops::RangeInclusive;
2
3use bevy::ecs::entity::{EntityHashMap, EntityHashSet};
4use bevy::ecs::query::QueryData;
5use bevy::prelude::*;
6use moonshine_core::prelude::{MapEntities, ReflectMapEntities};
7use rstar::{AABB, RTree, RTreeObject};
8use smallvec::SmallVec;
9
10use crate::entry::{self, EntryMode};
11use crate::graph::Node;
12use crate::settings::ProjectSettings;
13use crate::station::Station;
14use crate::trip::class::{Class, DisplayedStroke};
15use crate::units::time::Duration;
16use crate::vehicle::Vehicle;
17
18pub mod class;
19pub mod routing;
20
21pub struct TripPlugin;
22impl Plugin for TripPlugin {
23 fn build(&self, app: &mut App) {
24 app.add_plugins(routing::RoutingPlugin)
25 .init_resource::<TripSpatialIndex>()
26 .add_systems(Update, update_trip_spatial_index)
27 .add_observer(update_nominal_schedule)
28 .add_observer(convert_derived_entry_to_explicit)
29 .add_observer(update_add_trip_vehicles)
30 .add_observer(update_remove_trip_vehicles)
31 .add_observer(update_remove_vehicle_trips);
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct TripSpatialIndexItem {
37 pub trip: Entity,
38 pub entry0: Entity,
39 pub entry1: Entity,
40 pub t0: f64,
41 pub t1: f64,
42 pub t2: f64,
43 pub p0: [f64; 2],
44 pub p1: [f64; 2],
45}
46
47impl RTreeObject for TripSpatialIndexItem {
48 type Envelope = AABB<[f64; 3]>;
49
50 fn envelope(&self) -> Self::Envelope {
51 AABB::from_corners(
52 [
53 self.p0[0].min(self.p1[0]),
54 self.p0[1].min(self.p1[1]),
55 self.t0,
56 ],
57 [
58 self.p0[0].max(self.p1[0]),
59 self.p0[1].max(self.p1[1]),
60 self.t2,
61 ],
62 )
63 }
64}
65
66#[derive(Resource, Default)]
67pub struct TripSpatialIndex {
68 tree: RTree<TripSpatialIndexItem>,
69 entities: EntityHashMap<Vec<TripSpatialIndexItem>>,
70}
71
72impl TripSpatialIndex {
73 pub fn is_empty(&self) -> bool {
74 self.tree.size() == 0
75 }
76
77 pub fn query_xy_time(
78 &self,
79 x_range: RangeInclusive<f64>,
80 y_range: RangeInclusive<f64>,
81 time_range: RangeInclusive<f64>,
82 ) -> impl Iterator<Item = TripSpatialIndexItem> + '_ {
83 let x0 = (*x_range.start()).min(*x_range.end());
84 let x1 = (*x_range.start()).max(*x_range.end());
85 let y0 = (*y_range.start()).min(*y_range.end());
86 let y1 = (*y_range.start()).max(*y_range.end());
87 let t0 = (*time_range.start()).min(*time_range.end());
88 let t1 = (*time_range.start()).max(*time_range.end());
89
90 let envelope = AABB::from_corners([x0, y0, t0], [x1, y1, t1]);
91 self.tree
92 .locate_in_envelope_intersecting(&envelope)
93 .copied()
94 }
95
96 pub fn clear(&mut self) {
97 self.tree = RTree::new();
98 self.entities.clear();
99 }
100}
101
102fn update_trip_spatial_index(
103 mut index: ResMut<TripSpatialIndex>,
104 trips: Query<(Entity, &TripSchedule), With<Trip>>,
105 changed_trips: Query<Entity, Or<(Added<Trip>, Changed<TripSchedule>)>>,
106 changed_stops: Query<Entity, Or<(Added<entry::EntryStop>, Changed<entry::EntryStop>)>>,
107 changed_estimates: Query<
108 Entity,
109 Or<(Added<entry::EntryEstimate>, Changed<entry::EntryEstimate>)>,
110 >,
111 changed_nodes: Query<Entity, Or<(Added<Node>, Changed<Node>)>>,
112 mut removed_trips: RemovedComponents<Trip>,
113 mut removed_stop: RemovedComponents<entry::EntryStop>,
114 mut removed_estimate: RemovedComponents<entry::EntryEstimate>,
115 mut removed_node: RemovedComponents<Node>,
116 platform_q: Query<AnyOf<(&Station, &ChildOf)>>,
117 stop_q: Query<&entry::EntryStop>,
118 estimate_q: Query<&entry::EntryEstimate>,
119 node_q: Query<&Node>,
120 settings: Res<ProjectSettings>,
121) {
122 let mut to_remove_trips = EntityHashSet::default();
123
124 for entity in removed_trips.read() {
125 to_remove_trips.insert(entity);
126 }
127
128 let mut changed_trip_set = EntityHashSet::default();
129
130 for entity in &changed_trips {
131 changed_trip_set.insert(entity);
132 }
133
134 let has_changed_entries = !changed_stops.is_empty()
135 || !changed_estimates.is_empty()
136 || !changed_nodes.is_empty()
137 || removed_stop.read().next().is_some()
138 || removed_estimate.read().next().is_some()
139 || removed_node.read().next().is_some();
140
141 if has_changed_entries {
142 let changed_stops_set: EntityHashSet = changed_stops.iter().collect();
143 let changed_est_set: EntityHashSet = changed_estimates.iter().collect();
144 let changed_nodes_set: EntityHashSet = changed_nodes.iter().collect();
145
146 let check_entry = |entry: Entity| -> bool {
147 if changed_stops_set.contains(&entry) || changed_est_set.contains(&entry) {
148 return true;
149 }
150 if let Ok(stop) = stop_q.get(entry) {
151 let platform_entity = stop.entity();
152 if changed_nodes_set.contains(&platform_entity) {
153 return true;
154 }
155 if let Ok((_, Some(parent))) = platform_q.get(platform_entity) {
156 if changed_nodes_set.contains(&parent.parent()) {
157 return true;
158 }
159 }
160 }
161 false
162 };
163
164 for (trip_entity, schedule) in &trips {
165 if schedule.iter().any(|e| check_entry(*e)) {
166 changed_trip_set.insert(trip_entity);
167 }
168 }
169 }
170
171 for trip in to_remove_trips.iter() {
172 if let Some(old_items) = index.entities.remove(trip) {
173 for item in old_items {
174 index.tree.remove(&item);
175 }
176 }
177 }
178
179 if changed_trip_set.is_empty() && to_remove_trips.is_empty() {
180 return;
181 }
182
183 let get_station_xy = |entry_entity: Entity| -> Option<[f64; 2]> {
184 let platform_entity = stop_q.get(entry_entity).ok()?.entity();
185 let node = match platform_q.get(platform_entity).ok()? {
186 (Some(_), _) => node_q.get(platform_entity).ok()?,
187 (None, Some(parent)) => node_q.get(parent.parent()).ok()?,
188 _ => return None,
189 };
190 Some(node.coor.to_xy_arr())
191 };
192
193 let repeat_time = settings.repeat_frequency.0 as f64;
194
195 for trip_entity in changed_trip_set {
196 if to_remove_trips.contains(&trip_entity) {
197 continue;
198 }
199
200 if let Some(old_items) = index.entities.remove(&trip_entity) {
201 for item in old_items {
202 index.tree.remove(&item);
203 }
204 }
205
206 let Ok((_, schedule)) = trips.get(trip_entity) else {
207 continue;
208 };
209 if schedule.len() < 1 {
210 continue;
211 }
212
213 let mut new_items = Vec::new();
214
215 for pair in schedule.windows(2).chain(std::iter::once(
216 [schedule.last().unwrap().clone(); 2].as_slice(),
217 )) {
218 let [entry0, entry1] = pair else {
219 continue;
220 };
221 let entry0 = *entry0;
222 let entry1 = *entry1;
223
224 let Some(p0) = get_station_xy(entry0) else {
225 continue;
226 };
227 let Some(p1) = get_station_xy(entry1) else {
228 continue;
229 };
230
231 let Ok(estimate0) = estimate_q.get(entry0) else {
232 continue;
233 };
234 let Ok(estimate1) = estimate_q.get(entry1) else {
235 continue;
236 };
237
238 let t0 = estimate0.arr.0 as f64;
239 let t1 = estimate0.dep.0 as f64;
240 let t2 = (estimate1.arr.0 as f64).max(t1);
241
242 if repeat_time > 0.0 {
243 let dep_duration = t1 - t0;
244 let arr_duration = t2 - t0;
245 if arr_duration >= repeat_time {
246 new_items.push(TripSpatialIndexItem {
247 trip: trip_entity,
248 entry0,
249 entry1,
250 t0: 0.0,
251 t1: dep_duration.rem_euclid(repeat_time),
252 t2: repeat_time,
253 p0,
254 p1,
255 });
256 continue;
257 }
258
259 let normalized_t0 = t0.rem_euclid(repeat_time);
260 let normalized_t1 = normalized_t0 + dep_duration;
261 let normalized_t2 = normalized_t0 + arr_duration;
262 new_items.push(TripSpatialIndexItem {
263 trip: trip_entity,
264 entry0,
265 entry1,
266 t0: normalized_t0,
267 t1: normalized_t1,
268 t2: normalized_t2,
269 p0,
270 p1,
271 });
272
273 if normalized_t2 > repeat_time {
274 new_items.push(TripSpatialIndexItem {
275 trip: trip_entity,
276 entry0,
277 entry1,
278 t0: normalized_t0 - repeat_time,
279 t1: normalized_t1 - repeat_time,
280 t2: normalized_t2 - repeat_time,
281 p0,
282 p1,
283 });
284 }
285 } else {
286 new_items.push(TripSpatialIndexItem {
287 trip: trip_entity,
288 entry0,
289 entry1,
290 t0,
291 t1,
292 t2,
293 p0,
294 p1,
295 });
296 }
297 }
298
299 for item in &new_items {
300 index.tree.insert(*item);
301 }
302 index.entities.insert(trip_entity, new_items);
303 }
304}
305
306#[derive(Reflect, Component)]
308#[reflect(Component)]
309#[require(TripVehicles, TripSchedule, Name)]
310pub struct Trip;
311
312#[derive(Bundle)]
314pub struct TripBundle {
315 trip: Trip,
316 vehicles: TripVehicles,
317 name: Name,
318 class: TripClass,
319 nominal_schedule: TripNominalSchedule,
320}
321
322impl TripBundle {
323 pub fn new(name: &str, class: TripClass, nominal_schedule: Vec<Entity>) -> Self {
324 Self {
325 trip: Trip,
326 vehicles: TripVehicles::default(),
327 name: Name::from(name),
328 class,
329 nominal_schedule: TripNominalSchedule(nominal_schedule),
330 }
331 }
332}
333
334#[derive(Reflect, Component)]
336#[reflect(Component)]
337pub struct IsTimingReference;
338
339#[derive(Default, Reflect, Component, MapEntities, Deref, DerefMut)]
341#[component(map_entities)]
342#[reflect(Component, MapEntities)]
343pub struct TripVehicles(#[entities] pub SmallVec<[Entity; 1]>);
344
345#[derive(Reflect, Component, MapEntities, Deref, DerefMut)]
347#[component(map_entities)]
348#[reflect(Component, MapEntities)]
349#[relationship(relationship_target = class::Class)]
350#[require(Name)]
351pub struct TripClass(#[entities] pub Entity);
352
353#[derive(Reflect, Component, MapEntities, Deref, DerefMut)]
354#[component(map_entities)]
355#[reflect(Component, MapEntities)]
356pub struct TripNominalSchedule(#[entities] pub Vec<Entity>);
357
358#[derive(Reflect, Default, Component, MapEntities, Deref, DerefMut)]
359#[component(map_entities)]
360#[reflect(Component, MapEntities)]
361pub struct TripSchedule(#[entities] pub Vec<Entity>);
362
363#[derive(Debug, EntityEvent)]
364pub struct ConvertDerivedEntryToExplicit {
365 pub entity: Entity,
366}
367
368#[derive(QueryData)]
370pub struct TripQuery {
371 trip: &'static Trip,
372 pub entity: Entity,
373 pub vehicles: &'static TripVehicles,
374 pub name: &'static Name,
375 pub class: &'static TripClass,
376 pub schedule: &'static TripSchedule,
377}
378
379impl<'w, 's> TripQueryItem<'w, 's> {
380 pub fn duration<'a>(&self, q: &Query<'a, 'a, &entry::EntryEstimate>) -> Option<Duration> {
385 let beg = self.schedule.first().cloned()?;
386 let end = self.schedule.last().cloned()?;
387 let end_t = q.get(end).ok()?;
388 let beg_t = q.get(beg).ok()?;
389 Some(end_t.dep - beg_t.arr)
390 }
391 pub fn stroke<'a>(&self, q: &Query<'a, 'a, &DisplayedStroke, With<Class>>) -> DisplayedStroke {
392 q.get(self.class.entity()).unwrap().clone()
393 }
394}
395
396fn update_nominal_schedule(
397 msg: On<Remove, EntryMode>,
398 parent_q: Query<&ChildOf>,
399 mut schedule_q: Query<&mut TripNominalSchedule>,
400) {
401 let Ok(parent) = parent_q.get(msg.entity) else {
402 return;
403 };
404 let Ok(mut schedule) = schedule_q.get_mut(parent.parent()) else {
405 return;
406 };
407 if let Some(idx) = schedule.iter().position(|e| *e == msg.entity) {
408 schedule.remove(idx);
409 }
410}
411
412fn convert_derived_entry_to_explicit(
413 msg: On<ConvertDerivedEntryToExplicit>,
414 mut commands: Commands,
415 parent_q: Query<&ChildOf>,
416 schedule_q: Query<&TripSchedule, With<Trip>>,
417 mut nominal_q: Query<&mut TripNominalSchedule, With<Trip>>,
418) {
419 let entry = msg.entity;
420 let parent = parent_q.get(entry).unwrap();
421 let trip = parent.parent();
422 let schedule = schedule_q.get(trip).unwrap();
423 let mut nominal = nominal_q.get_mut(trip).unwrap();
424
425 if !nominal.iter().any(|e| *e == entry) {
426 let Some(schedule_idx) = schedule.iter().position(|e| *e == entry) else {
427 nominal.push(entry);
428 commands.entity(entry).remove::<entry::IsDerivedEntry>();
429 return;
430 };
431
432 let prev_nominal = schedule[..schedule_idx]
433 .iter()
434 .rev()
435 .find(|candidate| nominal.iter().any(|e| e == *candidate))
436 .copied();
437 let next_nominal = schedule[schedule_idx + 1..]
438 .iter()
439 .find(|candidate| nominal.iter().any(|e| e == *candidate))
440 .copied();
441
442 let insert_idx = if let Some(next) = next_nominal {
443 nominal
444 .iter()
445 .position(|e| *e == next)
446 .unwrap_or(nominal.len())
447 } else if let Some(prev) = prev_nominal {
448 nominal
449 .iter()
450 .position(|e| *e == prev)
451 .map(|i| i + 1)
452 .unwrap_or(nominal.len())
453 } else {
454 nominal.len()
455 };
456
457 nominal.insert(insert_idx, entry);
458 }
459
460 commands.entity(entry).remove::<entry::IsDerivedEntry>();
461}
462
463fn update_remove_trip_vehicles(
466 removed_vehicle: On<Remove, Vehicle>,
467 mut trips: Populated<&mut TripVehicles>,
468 vehicles: Query<&Vehicle>,
469) {
470 let veh = removed_vehicle.entity;
471 let Ok(Vehicle {
472 trips: remove_pending,
473 }) = vehicles.get(veh)
474 else {
475 return;
476 };
477 for &pending in remove_pending {
478 let Ok(mut trip_vehicles) = trips.get_mut(pending) else {
479 return;
480 };
481 trip_vehicles.retain(|v| *v != veh);
482 }
483}
484
485fn update_add_trip_vehicles(
488 removed_vehicle: On<Add, Vehicle>,
489 mut trips: Populated<&mut TripVehicles>,
490 vehicles: Query<&Vehicle>,
491) {
492 let veh = removed_vehicle.entity;
493 let Ok(Vehicle { trips: add_pending }) = vehicles.get(veh) else {
494 return;
495 };
496 for pending in add_pending.iter().copied() {
497 let Ok(mut trip_vehicles) = trips.get_mut(pending) else {
498 return;
499 };
500 trip_vehicles.push(veh);
501 }
502}
503
504fn update_remove_vehicle_trips(
507 removed_trip: On<Remove, TripVehicles>,
508 mut vehicles: Populated<&mut Vehicle>,
509 trips: Query<&TripVehicles>,
510) {
511 let trip = removed_trip.entity;
512 let Ok(remove_pending) = trips.get(trip) else {
513 return;
514 };
515 for &pending in &remove_pending.0 {
516 let Ok(mut trip_vehicles) = vehicles.get_mut(pending) else {
517 return;
518 };
519 trip_vehicles.trips.retain(|v| *v != trip);
520 }
521}