paiagram/rw_data/
qetrc.rs

1use bevy::{platform::collections::HashMap, prelude::*};
2use moonshine_core::kind::*;
3use serde::Deserialize;
4use serde_json;
5
6use crate::{
7    graph::{Graph, Interval, Station as IntervalStation},
8    lines::DisplayedLine,
9    rw_data::ModifyData,
10    units::{distance::Distance, time::TimetableTime},
11    vehicles::{
12        entries::{TravelMode, VehicleSchedule},
13        services::VehicleService,
14        vehicle_set::VehicleSet,
15    },
16};
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 [`VehicleService`] in Paiagram.
24    #[serde(rename = "trains")]
25    services: Vec<Service>,
26    // qETRC has the line field and the lines array, both contains line data.
27    // pyETRC only has the `line` field, while qETRC uses both to support multiple lines.
28    // To keep compatibility with pyETRC, we keep the `line` field as is,
29    // The lines would be chained together later with std::iter::once and chain
30    /// A single [`Line`]
31    line: Line,
32    /// Additional [`Line`]s. This field does not exist in pyETRC, only in qETRC.
33    lines: Option<Vec<Line>>,
34    /// Vehicles in the qETRC data.
35    /// They are named "circuits" in the original qETRC data. A "circuit" refers to a train that runs a set of services
36    /// in a given period, which matches the concept of [`Vehicle`] or [`VehicleSchedule`] in Paiagram.
37    #[serde(rename = "circuits")]
38    vehicles: Vec<Vehicle>,
39}
40
41/// A line that is used as the foundation of connection in qETRC data
42#[derive(Deserialize)]
43struct Line {
44    /// The name of the line
45    name: String,
46    /// [`Station`]s on the line.
47    stations: Vec<Station>,
48}
49
50#[derive(Deserialize)]
51struct Station {
52    /// Station name
53    #[serde(rename = "zhanming")]
54    name: String,
55    /// Distance from the start of the line, in kilometers
56    #[serde(rename = "licheng")]
57    distance_km: f32,
58}
59
60#[derive(Deserialize)]
61struct Service {
62    /// Each service may have multiple service numbers.
63    /// In qETRC's case, the first service number is always the main one, and we use that one in Paiagram.
64    #[serde(rename = "checi")]
65    service_number: Vec<String>,
66    // #[serde(rename = "type")]
67    // service_type: String,
68    /// The timetable entries of the service
69    timetable: Vec<TimetableEntry>,
70}
71
72#[derive(Deserialize)]
73struct TimetableEntry {
74    /// Whether the train would stop and load/unload passengers or freight at the station.
75    #[serde(rename = "business")]
76    would_stop: Option<bool>,
77    /// Arrival time in "HH:MM" format. "ddsj" in the original qETRC data refers to "到达时间".
78    #[serde(rename = "ddsj")]
79    arrival: String,
80    /// Departure time in "HH:MM" format. "cfsj" in the original qETRC data refers to "出发时间".
81    #[serde(rename = "cfsj")]
82    departure: String,
83    /// Station name
84    #[serde(rename = "zhanming")]
85    station_name: String,
86}
87
88#[derive(Deserialize)]
89struct Vehicle {
90    /// Vehicle model
91    #[serde(rename = "model")]
92    make: String,
93    /// Vehicle name
94    name: String,
95    /// Services that the vehicle runs.
96    #[serde(rename = "order")]
97    services: Vec<VehicleServiceEntry>,
98}
99
100#[derive(Deserialize)]
101struct VehicleServiceEntry {
102    /// Service number of the service
103    #[serde(rename = "checi")]
104    service_number: String,
105}
106
107struct ProcessedEntry {
108    arrival: TimetableTime,
109    departure: TimetableTime,
110    station_entity: Instance<IntervalStation>,
111    service_entity: Instance<VehicleService>,
112}
113
114pub fn load_qetrc(
115    mut commands: Commands,
116    mut reader: MessageReader<ModifyData>,
117    mut graph: ResMut<Graph>,
118) {
119    let mut str: Option<&str> = None;
120    for modification in reader.read() {
121        match modification {
122            ModifyData::LoadQETRC(s) => str = Some(s.as_str()),
123            _ => {}
124        }
125    }
126    let Some(str) = str else {
127        return;
128    };
129    let root: Root = match serde_json::from_str(str) {
130        Ok(r) => r,
131        // TODO: handle warning better
132        // TODO: add log page and warning banner
133        Err(e) => {
134            warn!("Failed to parse QETRC data: {e:?}");
135            return;
136        }
137    };
138    let lines_iter = std::iter::once(root.line).chain(root.lines.into_iter().flatten());
139    let mut station_map: HashMap<String, Instance<crate::graph::Station>> = HashMap::new();
140    fn make_station(
141        name: String,
142        commands: &mut Commands,
143        station_map: &mut HashMap<String, Instance<crate::graph::Station>>,
144        graph: &mut Graph,
145    ) -> Instance<crate::graph::Station> {
146        if let Some(&entity) = station_map.get(&name) {
147            return entity;
148        }
149        let station_entity = commands
150            .spawn(Name::new(name.clone()))
151            .insert_instance(crate::graph::Station::default())
152            .into();
153        station_map.insert(name, station_entity);
154        graph.add_node(station_entity);
155        station_entity
156    }
157    for line in lines_iter {
158        let mut entity_distances: Vec<(Instance<crate::graph::Station>, f32)> =
159            Vec::with_capacity(line.stations.len());
160        for station in line.stations {
161            let e = make_station(station.name, &mut commands, &mut station_map, &mut graph);
162            entity_distances.push((e, station.distance_km));
163        }
164        for w in entity_distances.windows(2) {
165            let [(prev, prev_d), (this, this_d)] = w else {
166                unreachable!()
167            };
168            // TODO: handle one way stations and intervals
169            let e1 = commands
170                .spawn_instance(Interval {
171                    speed_limit: None,
172                    length: Distance::from_km((this_d - prev_d).abs()),
173                })
174                .into();
175            let e2 = commands
176                .spawn_instance(Interval {
177                    speed_limit: None,
178                    length: Distance::from_km((this_d - prev_d).abs()),
179                })
180                .into();
181            graph.add_edge(*prev, *this, e1);
182            graph.add_edge(*this, *prev, e2);
183        }
184        let mut previous_distance_km = entity_distances.first().map_or(0.0, |(_, d)| *d);
185        for (_, distance_km) in entity_distances.iter_mut().skip(1) {
186            let current_distance_km = *distance_km;
187            *distance_km -= previous_distance_km;
188            previous_distance_km = current_distance_km;
189        }
190        // create a new displayed line
191        commands.spawn((
192            Name::new(line.name),
193            DisplayedLine::new(entity_distances.into_iter().map(|(e, d)| (e, d)).collect()),
194        ));
195    }
196    let mut service_pool: HashMap<String, Vec<ProcessedEntry>> =
197        HashMap::with_capacity(root.services.len());
198    for service in root.services {
199        let service_name = service
200            .service_number
201            .get(0)
202            .cloned()
203            .unwrap_or("<Unnamed>".into());
204        // TODO: handle class
205        let service_entity = commands
206            .spawn((Name::new(service_name.clone()),))
207            .insert_instance(VehicleService { class: None })
208            .into();
209        let mut processed_entries: Vec<ProcessedEntry> =
210            Vec::with_capacity(service.timetable.len());
211        for entry in service.timetable {
212            let station_entity = make_station(
213                entry.station_name,
214                &mut commands,
215                &mut station_map,
216                &mut graph,
217            );
218            let a = TimetableTime::from_str(&entry.arrival).unwrap_or_default();
219            let d = TimetableTime::from_str(&entry.departure).unwrap_or_default();
220            processed_entries.push(ProcessedEntry {
221                arrival: a,
222                departure: d,
223                station_entity,
224                service_entity,
225            });
226        }
227        service_pool.insert(service_name, processed_entries);
228    }
229    let vehicle_set_entity = commands
230        .spawn((Name::new("qETRC Vehicle Set"), VehicleSet))
231        .id();
232    for vehicle in root.vehicles {
233        let processed_entries: Vec<ProcessedEntry> = vehicle
234            .services
235            .iter()
236            .filter_map(|s| service_pool.remove(&s.service_number))
237            .flatten()
238            .collect();
239        make_vehicle(
240            format!("{} [{}]", vehicle.name, vehicle.make),
241            &mut commands,
242            processed_entries,
243            vehicle_set_entity,
244        );
245    }
246    for (service_name, entries) in service_pool {
247        make_vehicle(service_name, &mut commands, entries, vehicle_set_entity);
248    }
249}
250
251fn make_vehicle(
252    name: String,
253    commands: &mut Commands,
254    mut processed_entries: Vec<ProcessedEntry>,
255    vehicle_set_entity: Entity,
256) {
257    let vehicle_entity = commands
258        .spawn((Name::new(name), crate::vehicles::Vehicle))
259        .id();
260    commands
261        .entity(vehicle_set_entity)
262        .add_child(vehicle_entity);
263    let mut entry_entites: Vec<Entity> = Vec::with_capacity(processed_entries.len());
264    super::normalize_times(
265        processed_entries
266            .iter_mut()
267            .flat_map(|t| std::iter::once(&mut t.arrival).chain(std::iter::once(&mut t.departure))),
268    );
269    for ps in processed_entries {
270        let (arrival_mode, departure_mode) = if ps.arrival == ps.departure {
271            (TravelMode::At(ps.arrival), None)
272        } else {
273            (
274                TravelMode::At(ps.arrival),
275                Some(TravelMode::At(ps.departure)),
276            )
277        };
278        let entry_entity = commands
279            .spawn(crate::vehicles::entries::TimetableEntry {
280                arrival: arrival_mode,
281                departure: departure_mode,
282                station: ps.station_entity.entity(),
283                service: Some(ps.service_entity),
284                track: None,
285            })
286            .id();
287        commands.entity(vehicle_entity).add_child(entry_entity);
288        entry_entites.push(entry_entity);
289    }
290    commands.entity(vehicle_entity).insert(VehicleSchedule {
291        entities: entry_entites,
292        ..Default::default()
293    });
294}