Skip to main content

paiagram_core/
trip.rs

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
123// TODO: replace the dirty method with specific updates
124fn 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            // include the previous arr time
213            let t0 = estimate0.arr.0 as f64;
214            let t1 = estimate0.dep.0 as f64;
215            // we do a .max(t1) here to make that the last entry gets included properly
216            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/// Marker component for a trip
294#[derive(Reflect, Component)]
295#[reflect(Component)]
296#[require(TripVehicles, TripSchedule, Name)]
297pub struct Trip;
298
299/// Trip bundle.
300#[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/// Marker component for timing reference trips.
322#[derive(Reflect, Component)]
323#[reflect(Component)]
324pub struct IsTimingReference;
325
326/// A trip in the world
327#[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/// The class of the trip
333#[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/// Common query data for trips
356#[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    /// The duration of the trip, from the first entry's arrival time to the last entry's
368    /// departure time. This method only checks the first and last entries' times, hence
369    /// any intermediate entries are not considered.
370    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
449/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
450/// This removes vehicles from trip data.
451fn 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
471/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
472/// This adds vehicles into trip data.
473fn 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
490/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
491/// This removes trips from vehicle data.
492fn 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}