paiagram/vehicles/
entries.rs

1use crate::{
2    graph::{Graph, Interval, Station, StationEntries},
3    units::time::{Duration, TimetableTime},
4    vehicles::{AdjustTimetableEntry, services::VehicleService},
5};
6use bevy::platform::collections::HashSet;
7use bevy::prelude::*;
8use either::Either;
9use moonshine_core::kind::Instance;
10use moonshine_core::save::prelude::*;
11use petgraph::algo::astar;
12use smallvec::{SmallVec, smallvec};
13
14/// How the vehicle travels from/to the station.
15#[derive(Reflect, Debug, Default, Clone, Copy)]
16pub enum TravelMode {
17    /// The vehicle travels to or stops at the station at a determined time.
18    At(TimetableTime),
19    /// The vehicle travels to or stops at the station relative to the previous running/stopping time.
20    For(Duration),
21    /// The time is flexible and calculated.
22    /// This could be e.g. for flyover stops or less important intermediate stations.
23    #[default]
24    Flexible,
25}
26
27impl TravelMode {
28    pub fn adjust_time(&mut self, adjustment: Duration) {
29        match self {
30            TravelMode::At(t) => {
31                t.0 += adjustment.0;
32            }
33            TravelMode::For(dur) => {
34                dur.0 += adjustment.0;
35            }
36            TravelMode::Flexible => (),
37        }
38    }
39}
40
41impl std::fmt::Display for TravelMode {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::At(t) => write!(f, "{}", t),
45            Self::For(dur) => write!(f, "{}", dur),
46            Self::Flexible => write!(f, ">>"),
47        }
48    }
49}
50
51/// An entry in the timetable
52#[derive(Debug, Reflect, Component, Clone)]
53#[reflect(Component, MapEntities)]
54#[require(TimetableEntryCache, Save)]
55#[component(map_entities)]
56#[relationship(relationship_target = StationEntries)]
57pub struct TimetableEntry {
58    /// The node the vehicle stops at or passes.
59    #[relationship]
60    pub station: Entity,
61    /// How would the vehicle arrive at a station.
62    pub arrival: TravelMode,
63    /// How would the vehicle depart from a station. A `None` value means that the vehicle does not stop at the station.
64    pub departure: Option<TravelMode>,
65    /// The service the entry belongs to.
66    pub service: Option<Instance<VehicleService>>,
67    /// The track/platform/dock/berth etc. at the station.
68    pub track: Option<Entity>,
69}
70
71impl MapEntities for TimetableEntry {
72    fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
73        self.station.map_entities(entity_mapper);
74        self.track.map_entities(entity_mapper);
75        self.service.map_entities(entity_mapper);
76    }
77}
78
79impl TimetableEntry {
80    pub fn station(&self) -> Instance<Station> {
81        // SAFETY: station is always a valid Station entity
82        unsafe { Instance::from_entity_unchecked(self.station) }
83    }
84}
85
86#[derive(Reflect, Debug, Component, Default)]
87#[reflect(Component)]
88pub struct TimetableEntryCache {
89    pub estimate: Option<TimeEstimate>,
90}
91
92#[derive(Reflect, Debug)]
93pub struct TimeEstimate {
94    /// Estimate of the arrival time. This would be filled in during runtime. An estimate of `None` means that the
95    /// arrival time cannot be determined.
96    pub arrival: TimetableTime,
97    /// Estimate of the departure time. This would be filled in during runtime. An estimate of `None` means that the
98    /// departure time cannot be determined.
99    pub departure: TimetableTime,
100}
101
102/// A vehicle's schedule and departure pattern
103#[derive(Reflect, Debug, Component)]
104#[component(map_entities)]
105#[reflect(Component, MapEntities)]
106#[require(VehicleScheduleCache)]
107pub struct VehicleSchedule {
108    /// When would the schedule start.
109    pub start: TimetableTime,
110    /// How frequent would the schedule repeat. A value of `None` indicates that the schedule would not repeat.
111    pub repeat: Option<Duration>,
112    /// When would the vehicle depart. The departure times are relative to the start of the schedule.
113    /// This should always be sorted
114    /// In this case, this stores the departure time relative to the starting time.
115    pub departures: Vec<Duration>,
116    /// The timetable entities the schedule holds, in order.
117    pub entities: Vec<Entity>,
118}
119
120impl MapEntities for VehicleSchedule {
121    fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
122        for e in self.entities.iter_mut() {
123            e.map_entities(entity_mapper);
124        }
125    }
126}
127
128#[derive(Reflect, Debug, Clone, Copy)]
129pub enum ActualRouteEntry {
130    Nominal(Entity),
131    Derived(Entity),
132}
133
134impl ActualRouteEntry {
135    pub fn inner(self) -> Entity {
136        match self {
137            Self::Nominal(e) => e,
138            Self::Derived(e) => e,
139        }
140    }
141    pub fn inner_mut(&mut self) -> &mut Entity {
142        match self {
143            Self::Nominal(e) => e,
144            Self::Derived(e) => e,
145        }
146    }
147}
148
149#[derive(Component, Reflect, Debug, Default)]
150#[reflect(Component, MapEntities)]
151#[component(map_entities)]
152pub struct VehicleScheduleCache {
153    pub actual_route: Option<Vec<ActualRouteEntry>>,
154    /// Service entities indices. This piece of data is calculated during runtime.
155    /// This should always be sorted by Entity
156    // TODO: move service entities into their own component
157    #[reflect(ignore)]
158    pub service_entities: Vec<(
159        Instance<VehicleService>,
160        SmallVec<[std::ops::Range<usize>; 1]>,
161    )>,
162}
163
164impl MapEntities for VehicleScheduleCache {
165    fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
166        if let Some(actual_route) = &mut self.actual_route {
167            for entry in actual_route.iter_mut() {
168                entry.inner_mut().map_entities(entity_mapper);
169            }
170        }
171    }
172}
173
174impl VehicleScheduleCache {
175    pub fn position<'a>(
176        &self,
177        time: f32,
178        get_info: impl Fn(Entity) -> Option<(&'a TimetableEntry, &'a TimetableEntryCache)>,
179    ) -> Option<Either<(Entity, Entity, f32), Entity>> {
180        let actual_route = self.actual_route.as_ref()?;
181        let i = actual_route.iter().rposition(|e| {
182            let Some((_, cache)) = get_info(e.inner()) else {
183                return false;
184            };
185            let Some(entry_time) = cache.estimate.as_ref().map(|est| est.arrival) else {
186                return false;
187            };
188            entry_time.0 <= time as i32
189        })?;
190        let (this_entry, this_cache) = get_info(actual_route[i].inner())?;
191        let this_times = this_cache.estimate.as_ref()?;
192        if this_times.arrival.0 <= (time as i32) && (time as i32) <= this_times.departure.0 {
193            return Some(Either::Right(this_entry.station.entity()));
194        }
195        let (next_entry, next_cache) = get_info(actual_route.get(i + 1)?.inner())?;
196        let next_times = next_cache.estimate.as_ref()?;
197        let duration = next_times.arrival - this_times.departure;
198        let elapsed = time - this_times.departure.0 as f32;
199        if duration.0 <= 0 {
200            return Some(Either::Right(next_entry.station.entity()));
201        }
202        let factor = elapsed / (duration.0 as f32);
203        Some(Either::Left((
204            this_entry.station.entity(),
205            next_entry.station.entity(),
206            factor,
207        )))
208    }
209}
210
211pub fn calculate_actual_route(
212    mut vehicles: Query<(&mut VehicleScheduleCache, &VehicleSchedule)>,
213    mut msg_vehicle_changes: MessageReader<AdjustTimetableEntry>,
214    mut commands: Commands,
215    timetable_entries: Query<(&ChildOf, &TimetableEntry)>,
216    names: Query<&Name>,
217    graph: Res<Graph>,
218    intervals: Query<&Interval>,
219    mut actual_route_list: Local<Vec<ActualRouteEntry>>,
220    mut warned_pairs: Local<HashSet<(Instance<Station>, Instance<Station>)>>,
221    mut processed: Local<Vec<Entity>>,
222) {
223    processed.clear();
224    for vehicle_entity in msg_vehicle_changes
225        .read()
226        .filter_map(|msg| timetable_entries.get(msg.entity).ok().map(|(c, _)| c.0))
227    {
228        if processed.contains(&vehicle_entity) {
229            continue;
230        }
231        processed.push(vehicle_entity);
232        let Ok((mut cache, schedule)) = vehicles.get_mut(vehicle_entity) else {
233            continue;
234        };
235        let mut previous_route = cache.actual_route.take().unwrap_or_default();
236
237        // Derived entries are a cache: on any schedule change, just rebuild them.
238        for entry in previous_route.iter().copied() {
239            if let ActualRouteEntry::Derived(id) = entry {
240                commands.entity(id).despawn();
241            }
242        }
243
244        let mut prev_entry: Option<&TimetableEntry> = None;
245
246        actual_route_list.clear();
247        warned_pairs.clear();
248        for entity in schedule
249            .entities
250            .iter()
251            .copied()
252            .map(ActualRouteEntry::Nominal)
253        {
254            let Ok((_, entry)) = timetable_entries.get(entity.inner()) else {
255                continue;
256            };
257            let Some(prev) = prev_entry.replace(entry) else {
258                actual_route_list.push(entity);
259                continue;
260            };
261            if prev.station() == entry.station() {
262                actual_route_list.push(entity);
263                continue;
264            }
265            if graph.contains_edge(prev.station(), entry.station()) {
266                actual_route_list.push(entity);
267                continue;
268            }
269
270            // If either station isn't in the graph at all (e.g. schedule-only stations), routing
271            // is expected to fail. Don't spam warnings or run astar in this case.
272            let prev_in_graph = graph.contains_node(prev.station());
273            let next_in_graph = graph.contains_node(entry.station());
274            let service_name = prev
275                .service
276                .map(|e| names.get(e.entity()).ok())
277                .flatten()
278                .map(Name::as_str)
279                .unwrap_or("<unnammed>");
280            if !(prev_in_graph && next_in_graph) {
281                let pair = (prev.station(), entry.station());
282                if warned_pairs.insert(pair) {
283                    let prev_name = names
284                        .get(prev.station().entity())
285                        .ok()
286                        .map(Name::as_str)
287                        .unwrap_or("<unnamed>");
288                    let next_name = names
289                        .get(entry.station().entity())
290                        .ok()
291                        .map(Name::as_str)
292                        .unwrap_or("<unnamed>");
293                    warn!(
294                        "Skipping routing for timetable stations of {service_name} not in graph: {prev_name}({:?}) -> {next_name}({:?}); nodes_in_graph=({prev_in_graph},{next_in_graph})",
295                        prev.station(),
296                        entry.station()
297                    );
298                }
299                actual_route_list.push(entity);
300                continue;
301            }
302            // compare the stuff between the last and current entries
303            let Some((_, path)) = astar(
304                &graph.inner(),
305                graph.node_index(prev.station()).unwrap(),
306                |finish| finish == graph.node_index(entry.station()).unwrap(),
307                |edge| {
308                    if let Ok(interval) = intervals.get(edge.weight().entity()) {
309                        interval.length.0
310                    } else {
311                        i32::MAX
312                    }
313                },
314                |_| 0,
315            ) else {
316                // This can happen if the graph is disconnected or if station names don't match
317                // between timetable and line data. Log a single warning per pair per rebuild.
318                let pair = (prev.station(), entry.station());
319                if warned_pairs.insert(pair) {
320                    let prev_name = names
321                        .get(prev.station().entity())
322                        .ok()
323                        .map(Name::as_str)
324                        .unwrap_or("<unnamed>");
325                    let next_name = names
326                        .get(entry.station().entity())
327                        .ok()
328                        .map(Name::as_str)
329                        .unwrap_or("<unnamed>");
330                    warn!(
331                        "No route in graph between consecutive timetable stations: {prev_name}({:?}) -> {next_name}({:?})",
332                        prev.station(),
333                        entry.station()
334                    );
335                }
336                actual_route_list.push(entity);
337                continue;
338            };
339            if path.len() >= 2 {
340                for &node_index in &path[1..path.len() - 1] {
341                    let station_entity = graph.entity(node_index).unwrap();
342                    let station_name = names
343                        .get(station_entity.entity())
344                        .ok()
345                        .map(Name::as_str)
346                        .unwrap_or("<unnamed>");
347                    info!(?station_name, ?service_name);
348                    let derived_entity = commands
349                        .spawn((TimetableEntry {
350                            station: station_entity.entity(),
351                            service: prev.service,
352                            arrival: TravelMode::Flexible,
353                            departure: None,
354                            track: None,
355                        },))
356                        .id();
357                    commands.entity(vehicle_entity).add_child(derived_entity);
358                    actual_route_list.push(ActualRouteEntry::Derived(derived_entity));
359                }
360            }
361            actual_route_list.push(entity);
362        }
363
364        previous_route.clear();
365        previous_route.extend(actual_route_list.iter());
366        cache.actual_route = Some(previous_route);
367    }
368}
369
370pub fn populate_services(
371    mut msg_reader: MessageReader<super::AdjustTimetableEntry>,
372    mut schedules: Populated<(&mut VehicleScheduleCache, &VehicleSchedule)>,
373    entries: Populated<(&TimetableEntry, &ChildOf)>,
374) {
375    for msg in msg_reader.read() {
376        let super::AdjustTimetableEntry { entity, .. } = msg;
377        let Ok((_, parent)) = entries.get(*entity) else {
378            continue;
379        };
380        let Ok((mut schedule_cache, schedule)) = schedules.get_mut(parent.0) else {
381            continue;
382        };
383        let mut pool: Vec<(
384            Instance<VehicleService>,
385            SmallVec<[std::ops::Range<usize>; 1]>,
386        )> = Vec::new();
387        let mut start: usize = 0;
388        let mut previous_service: Option<Instance<VehicleService>> = None;
389        for (idx, entry_entity) in schedule.entities.iter().enumerate() {
390            let Ok((entry, _)) = entries.get(*entry_entity) else {
391                start = idx;
392                continue;
393            };
394            let Some(current_service) = entry.service else {
395                start = idx;
396                continue;
397            };
398
399            if let Some(prev) = previous_service {
400                if prev != current_service {
401                    match pool.binary_search_by_key(&prev, |(e, _)| *e) {
402                        Ok(j) => {
403                            pool[j].1.push(start..idx);
404                        }
405                        Err(j) => {
406                            pool.insert(j, (prev, smallvec![start..idx]));
407                        }
408                    }
409                    start = idx;
410                    previous_service = Some(current_service);
411                }
412            } else {
413                previous_service = Some(current_service);
414                start = idx;
415            }
416        }
417        if let Some(prev) = previous_service {
418            let idx = schedule.entities.len();
419            match pool.binary_search_by_key(&prev, |(e, _)| *e) {
420                Ok(j) => {
421                    pool[j].1.push(start..idx);
422                }
423                Err(j) => {
424                    pool.insert(j, (prev, smallvec![start..idx]));
425                }
426            }
427        }
428        schedule_cache.service_entities = pool;
429    }
430}
431
432impl Default for VehicleSchedule {
433    fn default() -> Self {
434        Self {
435            start: TimetableTime(0),
436            repeat: Some(Duration(86400)),
437            departures: vec![Duration(0)],
438            entities: Vec::new(),
439        }
440    }
441}
442
443impl VehicleSchedule {
444    pub fn into_entries<'a, F>(
445        &self,
446        mut lookup: F,
447    ) -> impl Iterator<Item = (&'a TimetableEntry, Entity)>
448    where
449        F: FnMut(Entity) -> Option<&'a TimetableEntry> + 'a,
450    {
451        self.entities
452            .iter()
453            .filter_map(move |e| lookup(*e).map(|r| (r, *e)))
454    }
455}
456
457impl VehicleScheduleCache {
458    pub fn get_service_entries(&self, service: Entity) -> Option<Vec<&[ActualRouteEntry]>> {
459        let Some(actual_route) = &self.actual_route else {
460            return None;
461        };
462        let i = self
463            .service_entities
464            .binary_search_by_key(&service, |(e, _)| e.entity());
465        let Ok(i) = i else { return None };
466        let (_, entries) = &self.service_entities[i];
467        let mut ret = Vec::with_capacity(entries.len());
468        for entry in entries {
469            ret.push(&actual_route[entry.clone()]);
470        }
471        Some(ret)
472    }
473    pub fn get_service_first_entry(&self, service: Entity) -> Option<ActualRouteEntry> {
474        let Some(actual_route) = &self.actual_route else {
475            return None;
476        };
477        let i = self
478            .service_entities
479            .binary_search_by_key(&service, |(e, _)| e.entity());
480        let Ok(i) = i else { return None };
481        return self.service_entities[i]
482            .1
483            .first()
484            .and_then(|e| Some(actual_route[e.start]));
485    }
486    pub fn get_service_last_entry(
487        &self,
488        service: Instance<VehicleService>,
489    ) -> Option<ActualRouteEntry> {
490        let Some(actual_route) = &self.actual_route else {
491            return None;
492        };
493        let i = self
494            .service_entities
495            .binary_search_by_key(&service, |(e, _)| *e);
496        let Ok(i) = i else { return None };
497        return self.service_entities[i]
498            .1
499            .last()
500            .and_then(|e| Some(actual_route[e.end.saturating_sub(1)]));
501    }
502
503    pub fn get_entries_range<'a, F>(
504        &self,
505        parent: &'a VehicleSchedule,
506        range: std::ops::Range<TimetableTime>,
507        mut lookup: F,
508    ) -> Option<
509        Vec<(
510            TimetableTime,
511            Vec<(
512                (&'a TimetableEntry, &'a TimetableEntryCache),
513                ActualRouteEntry,
514            )>,
515        )>,
516    >
517    where
518        F: FnMut(Entity) -> Option<(&'a TimetableEntry, &'a TimetableEntryCache)> + 'a,
519    {
520        let Some(actual_route) = &self.actual_route else {
521            return None;
522        };
523        let timetable_entries = actual_route
524            .iter()
525            .filter_map(move |e| lookup(e.inner()).map(|t| (t, *e)))
526            .collect::<Vec<_>>();
527        let mut ret = Vec::with_capacity(timetable_entries.len());
528        let repeats_iter = match parent.repeat {
529            None => Either::Left(std::iter::once(parent.start)),
530            Some(duration) => {
531                let start = parent.start
532                    + duration * {
533                        let main = (range.start - parent.start).0.div_euclid(duration.0);
534                        let last_dep = *parent.departures.last()?
535                            + timetable_entries
536                                .last()
537                                .map(|((_, tec), _)| tec.estimate.as_ref())??
538                                .departure
539                                .as_duration();
540                        let sub = last_dep.0.div_euclid(duration.0);
541                        main - sub
542                    };
543                Either::Right(std::iter::successors(Some(start), move |t| {
544                    let time = *t + duration;
545                    if time > range.end {
546                        return None;
547                    }
548                    Some(time)
549                }))
550            }
551        };
552        for base_time in repeats_iter {
553            for departure in parent.departures.iter().copied() {
554                let start_index = timetable_entries.iter().position(|((_, tec), _)| {
555                    tec.estimate.as_ref().map_or(false, |e| {
556                        departure + base_time + e.arrival.as_duration() > range.start
557                    })
558                });
559                let end_index = timetable_entries.iter().rposition(|((_, tec), _)| {
560                    tec.estimate.as_ref().map_or(false, |e| {
561                        departure + base_time + e.departure.as_duration() < range.end
562                    })
563                });
564                let (Some(mut si), Some(mut ei)) = (start_index, end_index) else {
565                    continue;
566                };
567                si = si.saturating_sub(1);
568                ei = (ei + 1).min(timetable_entries.len() - 1);
569                // this should not happen
570                if si > ei {
571                    continue;
572                }
573                let v = &timetable_entries[si..=ei];
574                ret.push((departure + base_time, v.to_vec()))
575            }
576        }
577        Some(ret)
578    }
579}