Skip to main content

paiagram_core/import/
oudia.rs

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