Skip to main content

paiagram_core/
trip.rs

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/// Marker component for a trip
307#[derive(Reflect, Component)]
308#[reflect(Component)]
309#[require(TripVehicles, TripSchedule, Name)]
310pub struct Trip;
311
312/// Trip bundle.
313#[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/// Marker component for timing reference trips.
335#[derive(Reflect, Component)]
336#[reflect(Component)]
337pub struct IsTimingReference;
338
339/// A trip in the world
340#[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/// The class of the trip
346#[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/// Common query data for trips
369#[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    /// The duration of the trip, from the first entry's arrival time to the
381    /// last entry's departure time. This method only checks the first and
382    /// last entries' times, hence any intermediate entries are not
383    /// considered.
384    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
463/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
464/// This removes vehicles from trip data.
465fn 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
485/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
486/// This adds vehicles into trip data.
487fn 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
504/// Helper function that manually synchronizes [`TripVehicles`] and [`Vehicle`].
505/// This removes trips from vehicle data.
506fn 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}