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 for (i, station) in route.stations.iter().enumerate() {
47 let station_entity =
49 super::make_station(&station.name, &mut station_map, &mut graph, &mut commands);
50 stations[i] = Some(station_entity);
51 }
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 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 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 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 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}