Skip to main content

paiagram_core/import/
oudia.rs

1use bevy::platform::collections::HashMap;
2use bevy::prelude::*;
3use itertools::Itertools;
4use moonshine_core::kind::*;
5use paiagram_oudia::{
6    Direction, ServiceMode, Time as OuDiaTime, TimetableEntry as OuDiaTimetableEntry,
7    parse_oud_to_ir, parse_oud2_to_ir,
8};
9
10use crate::colors::DisplayedColor;
11use crate::entry::{EntryBundle, TravelMode};
12use crate::graph::Graph;
13use crate::import::OuDiaContentType;
14use crate::route::Route;
15use crate::station::Station as StationComponent;
16use crate::trip::class::{Class as ClassComponent, ClassBundle, DisplayedStroke};
17use crate::trip::{TripBundle, TripClass};
18use crate::units::distance::Distance;
19use crate::units::time::TimetableTime;
20
21#[derive(Debug, Clone, Copy)]
22struct TimetableEntry {
23    service_mode: ServiceMode,
24    arrival_time: Option<TimetableTime>,
25    departure_time: Option<TimetableTime>,
26}
27
28impl From<OuDiaTime> for TimetableTime {
29    fn from(value: OuDiaTime) -> Self {
30        Self(value.seconds())
31    }
32}
33
34pub fn load_oud(msg: On<super::LoadOuDia>, mut commands: Commands, mut graph: ResMut<Graph>) {
35    info!("Loading OUD/OUD2 data...");
36    let root = match &msg.content {
37        OuDiaContentType::OuDiaSecond(s) => parse_oud2_to_ir(s),
38        OuDiaContentType::OuDia(d) => parse_oud_to_ir(d),
39    }
40    .expect("Failed to parse OUD/OUD2 data");
41    let route = root.route;
42    let mut station_map: HashMap<String, Instance<StationComponent>> = HashMap::new();
43    let mut stations: Vec<Option<Instance<StationComponent>>> = vec![None; route.stations.len()];
44    // let mut break_flags: Vec<bool> = Vec::with_capacity(route.stations.len());
45    for (i, station) in route.stations.iter().enumerate() {
46        // a bit slower but standardized
47        let station_entity =
48            super::make_station(&station.name, &mut station_map, &mut graph, &mut commands);
49        stations[i] = Some(station_entity);
50        // TODO: restore interval breaking mechanism
51        // break_flags.push(station.break_interval);
52    }
53
54    let station_instances: Vec<Instance<StationComponent>> =
55        stations.into_iter().map(|e| e.unwrap()).collect();
56    let class_instances: Vec<Entity> = route
57        .classes
58        .into_iter()
59        .map(|it| {
60            let [_, r, g, b] = it.diagram_line_color.0;
61            commands
62                .spawn(ClassBundle {
63                    class: ClassComponent::default(),
64                    name: Name::new(it.name),
65                    stroke: DisplayedStroke {
66                        color: DisplayedColor::Custom(egui::Color32::from_rgb(r, g, b)),
67                        width: 1.0,
68                    },
69                })
70                .id()
71        })
72        .collect();
73
74    let travel_durations: Vec<Option<OuDiaTime>> = route.diagrams[0]
75        .minimum_interval_durations(&route.stations)
76        .collect();
77
78    commands.spawn((
79        Name::new(route.name),
80        Route {
81            stops: station_instances.iter().map(|e| e.entity()).collect(),
82            lengths: travel_durations
83                .iter()
84                .map(|t| match t {
85                    // TODO: write a proper constant
86                    Some(t) => t.seconds() as f32 / 60.0 * 2.0,
87                    None => 1.0 * 2.0,
88                })
89                .collect(),
90        },
91    ));
92
93    for i in 0..station_instances.len().saturating_sub(1) {
94        // if break_flags[i] {
95        //     continue;
96        // }
97        super::add_interval_pair(
98            &mut graph,
99            &mut commands,
100            station_instances[i].entity(),
101            station_instances[i + 1].entity(),
102            travel_durations[i].map_or(Distance::from_m(1000), |it| {
103                Distance::from_m(it.seconds() / 60 * 1000)
104            }),
105        );
106    }
107
108    // TODO: find a method to support multiple diagrams
109    for diagram in route.diagrams.into_iter().take(1) {
110        for trip in diagram.trips {
111            let times: Vec<TimetableEntry> = trip
112                .times
113                .into_iter()
114                .map(convert_timetable_entry)
115                .collect();
116
117            let trip_class = class_instances[trip.class_index];
118
119            let mut times_chunked: Vec<_> = times
120                .into_iter()
121                .enumerate()
122                .filter_map(|(i, time)| {
123                    if matches!(time.service_mode, ServiceMode::NoOperation) {
124                        return None;
125                    }
126                    let station_index = match trip.direction {
127                        Direction::Down => i,
128                        Direction::Up => station_instances.len() - 1 - i,
129                    };
130                    let stop = station_instances[station_index];
131                    Some((stop, time))
132                })
133                .chunk_by(|(s, _t)| *s)
134                .into_iter()
135                .map(|(s, mut g)| {
136                    let (_, first_time) = g.next().unwrap();
137                    let mut group = [None; 2];
138                    group[0] = first_time.arrival_time;
139                    group[1] = first_time.departure_time;
140                    if let Some((_, last_time)) = g.last() {
141                        group[1] = last_time.departure_time;
142                    }
143                    (s, group, first_time.service_mode)
144                })
145                .collect();
146
147            super::normalize_times(times_chunked.iter_mut().flat_map(|(_, g, _)| g).flatten());
148
149            let nominal_entries: Vec<_> = times_chunked
150                .into_iter()
151                .map(|(stop, [arrival_time, departure_time], passing_mode)| {
152                    // in this case, this would consume the iterator.
153                    let arrival_mode = if matches!(passing_mode, ServiceMode::Pass) {
154                        None
155                    } else {
156                        Some(arrival_time.map_or(TravelMode::Flexible, |t| TravelMode::At(t)))
157                    };
158                    let departure_mode =
159                        departure_time.map_or(TravelMode::Flexible, |t| TravelMode::At(t));
160                    commands
161                        .spawn(EntryBundle::new(
162                            arrival_mode,
163                            departure_mode,
164                            stop.entity(),
165                        ))
166                        .id()
167                })
168                .collect();
169
170            commands
171                .spawn_empty()
172                .add_children(&nominal_entries)
173                .insert(TripBundle::new(
174                    &trip.name.unwrap_or("<??>".to_string()),
175                    TripClass(trip_class.entity()),
176                    nominal_entries,
177                ));
178        }
179    }
180}
181
182fn convert_timetable_entry(entry: OuDiaTimetableEntry) -> TimetableEntry {
183    TimetableEntry {
184        service_mode: entry.service_mode,
185        arrival_time: entry.arrival_time.map(TimetableTime::from),
186        departure_time: entry.departure_time.map(TimetableTime::from),
187    }
188}