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