Skip to main content

paiagram_core/import/
qetrc.rs

1use bevy::platform::collections::HashMap;
2use bevy::prelude::*;
3use egui::Color32;
4use moonshine_core::kind::*;
5use serde::Deserialize;
6use serde_json;
7
8use crate::colors::DisplayedColor;
9use crate::entry::{EntryBundle, TravelMode};
10use crate::graph::Graph;
11use crate::route::Route;
12use crate::station::Station;
13use crate::trip::class::{Class, ClassBundle, DisplayedStroke};
14use crate::trip::{TripBundle, TripClass};
15use crate::units::distance::Distance;
16use crate::units::time::TimetableTime;
17
18/// The root structure of the qETRC JSON data
19#[derive(Deserialize)]
20struct Root {
21    // qetrc_release: u32,
22    // qetrc_version: String,
23    /// Trains in the original qETRC data. Each "train" corresponds to a
24    /// [`crate::trip::Trip`] in Paiagram.
25    #[serde(rename = "trains")]
26    services: Vec<Service>,
27    // qETRC has the line field and the lines array, both contains line data.
28    // pyETRC only has the `line` field, while qETRC uses both to support multiple lines.
29    // To keep compatibility with pyETRC, we keep the `line` field as is,
30    // The lines would be chained together later with std::iter::once and chain
31    /// A single [`Line`]
32    line: Line,
33    /// Additional [`Line`]s. This field does not exist in pyETRC, only in
34    /// qETRC.
35    lines: Option<Vec<Line>>,
36    /// Vehicles in the qETRC data.
37    /// They are named "circuits" in the original qETRC data. A "circuit" refers
38    /// to a train that runs a set of services in a given period, which
39    /// matches the concept of [`Vehicle`] in Paiagram.
40    #[serde(rename = "circuits")]
41    vehicles: Vec<Vehicle>,
42    config: Option<Config>,
43}
44
45/// A line that is used as the foundation of connection in qETRC data
46#[derive(Deserialize)]
47struct Line {
48    /// The name of the line
49    name: String,
50    /// [`Station`]s on the line.
51    stations: Vec<QStation>,
52}
53
54#[derive(Deserialize)]
55struct QStation {
56    /// Station name
57    #[serde(rename = "zhanming")]
58    name: String,
59    /// Distance from the start of the line, in kilometers
60    #[serde(rename = "licheng")]
61    distance_km: f32,
62}
63
64#[derive(Deserialize)]
65struct Service {
66    /// Each service may have multiple service numbers.
67    /// In qETRC's case, the first service number is always the main one, and we
68    /// use that one in Paiagram.
69    #[serde(rename = "checi")]
70    service_number: Vec<String>,
71    #[serde(rename = "type")]
72    service_type: String,
73    /// The timetable entries of the service
74    timetable: Vec<TimetableEntry>,
75}
76
77#[derive(Deserialize)]
78struct TimetableEntry {
79    /// Whether the train would stop and load/unload passengers or freight at
80    /// the station.
81    #[serde(rename = "business")]
82    would_stop: Option<bool>,
83    /// Arrival time in "HH:MM" format. "ddsj" in the original qETRC data refers
84    /// to "到达时间".
85    #[serde(rename = "ddsj")]
86    arrival: String,
87    /// Departure time in "HH:MM" format. "cfsj" in the original qETRC data
88    /// refers to "出发时间".
89    #[serde(rename = "cfsj")]
90    departure: String,
91    /// Station name
92    #[serde(rename = "zhanming")]
93    station_name: String,
94}
95
96#[derive(Deserialize)]
97struct Vehicle {
98    /// Vehicle model
99    #[serde(rename = "model")]
100    make: String,
101    /// Vehicle name
102    name: String,
103    /// Services that the vehicle runs.
104    #[serde(rename = "order")]
105    services: Vec<VehicleServiceEntry>,
106}
107
108#[derive(Deserialize)]
109struct VehicleServiceEntry {
110    /// Service number of the service
111    #[serde(rename = "checi")]
112    service_number: String,
113}
114
115#[derive(Deserialize)]
116struct Config {
117    #[serde(default)]
118    default_colors: HashMap<String, String>,
119}
120
121pub fn load_qetrc(event: On<super::LoadQETRC>, mut commands: Commands, mut graph: ResMut<Graph>) {
122    let root: Root = match serde_json::from_str(&event.content) {
123        Ok(r) => r,
124        // TODO: handle warning better
125        // TODO: add log page and warning banner
126        Err(e) => {
127            warn!("Failed to parse QETRC data: {e:?}");
128            return;
129        }
130    };
131    let lines_iter = std::iter::once(root.line).chain(root.lines.into_iter().flatten());
132    let mut station_map: HashMap<String, Instance<Station>> = HashMap::new();
133    let mut class_map: HashMap<String, Instance<Class>> = HashMap::new();
134    if let Some(config) = root.config {
135        for (class, color) in config.default_colors {
136            // #RRGGBB
137            // 0123456
138            let (r, g, b) = (
139                u8::from_str_radix(&color[1..=2], 16).unwrap(),
140                u8::from_str_radix(&color[3..=4], 16).unwrap(),
141                u8::from_str_radix(&color[5..=6], 16).unwrap(),
142            );
143            super::make_class(&class, &mut class_map, &mut commands, || ClassBundle {
144                class: Class::default(),
145                name: Name::new(class.clone()),
146                stroke: DisplayedStroke {
147                    width: 1.0,
148                    color: DisplayedColor::Custom(Color32::from_rgb(r, g, b)),
149                },
150            });
151        }
152    }
153    for line in lines_iter {
154        let mut entity_distances: Vec<(Instance<Station>, f32)> =
155            Vec::with_capacity(line.stations.len());
156        for station in line.stations {
157            let e = super::make_station(&station.name, &mut station_map, &mut graph, &mut commands);
158            entity_distances.push((e, station.distance_km));
159        }
160        for w in entity_distances.windows(2) {
161            let [(prev, prev_d), (this, this_d)] = w else {
162                unreachable!()
163            };
164            // TODO: handle one way stations and intervals
165            let length = Distance::from_km((this_d - prev_d).abs());
166            super::add_interval_pair(
167                &mut graph,
168                &mut commands,
169                prev.entity(),
170                this.entity(),
171                length,
172            );
173        }
174        let mut previous_distance_km = entity_distances.first().map_or(0.0, |(_, d)| *d);
175        for (_, distance_km) in entity_distances.iter_mut().skip(1) {
176            let current_distance_km = *distance_km;
177            *distance_km -= previous_distance_km;
178            previous_distance_km = current_distance_km;
179        }
180        // create a new displayed line
181        commands.spawn((
182            Name::new(line.name),
183            Route {
184                stops: entity_distances.iter().map(|(e, _)| e.entity()).collect(),
185                lengths: entity_distances.iter().copied().map(|(_, d)| d).collect(),
186            },
187        ));
188    }
189    let mut trip_pool: HashMap<String, Entity> = HashMap::with_capacity(root.services.len());
190    for service in root.services {
191        let mut entries: Vec<_> = service
192            .timetable
193            .into_iter()
194            .map(|e| {
195                (
196                    TimetableTime::from_str(&e.arrival).unwrap(),
197                    TimetableTime::from_str(&e.departure).unwrap(),
198                    super::make_station(
199                        &e.station_name,
200                        &mut station_map,
201                        &mut graph,
202                        &mut commands,
203                    ),
204                )
205            })
206            .collect();
207        super::normalize_times(entries.iter_mut().flat_map(|(a, d, _)| [a, d]));
208        let trip_class =
209            super::make_class(&service.service_type, &mut class_map, &mut commands, || {
210                ClassBundle {
211                    class: Class::default(),
212                    name: Name::new(service.service_type.clone()),
213                    stroke: DisplayedStroke::from_seed(service.service_type.as_bytes()),
214                }
215            });
216        let nominal_entries: Vec<Entity> = entries
217            .into_iter()
218            .map(|(arr, dep, stop)| {
219                if dep < arr {
220                    info!(?arr, ?dep, ?service.service_number)
221                }
222                debug_assert!(dep >= arr);
223                let arr = (dep != arr).then(|| TravelMode::At(arr));
224                let dep = TravelMode::At(dep);
225                commands
226                    .spawn(EntryBundle::new(arr, dep, stop.entity()))
227                    .id()
228            })
229            .collect();
230        let trip_entity = commands
231            .spawn_empty()
232            .add_children(&nominal_entries)
233            .insert(TripBundle::new(
234                &service.service_number[0],
235                TripClass(trip_class.entity()),
236                nominal_entries,
237            ))
238            .id();
239        trip_pool.insert(service.service_number[0].clone(), trip_entity);
240    }
241    for vehicle in root.vehicles {
242        let vehicle_name = format!("{} [{}]", vehicle.name, vehicle.make);
243        let mut v = crate::vehicle::Vehicle::default();
244        for number in vehicle.services.iter().map(|it| &it.service_number) {
245            let Some(&e) = trip_pool.get(number) else {
246                warn!(
247                    "Vehicle {} has trip {} but the trip isn't in pool",
248                    vehicle_name, number
249                );
250                continue;
251            };
252            v.trips.push(e);
253        }
254        commands.spawn((Name::new(vehicle_name), v));
255    }
256}