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