paiagram/export/
typst_timetable.rs1use bevy::{ecs::system::RunSystemOnce, prelude::*};
2use itertools::Itertools;
3use moonshine_core::kind::Instance;
4use serde::Serialize;
5
6use crate::{
7 graph::{Graph, Station},
8 lines::DisplayedLine,
9 units::time::TimetableTime,
10 vehicles::entries::{
11 TimetableEntry, TimetableEntryCache, VehicleSchedule, VehicleScheduleCache,
12 },
13};
14pub struct TypstTimetable;
15
16#[derive(Default, Serialize)]
17#[serde(rename_all = "kebab-case")]
18struct Root {
19 stations: Vec<(String, StationSettings)>,
20 vehicles: Vec<ExportedVehicle>,
21}
22
23#[derive(Serialize)]
24struct StationSettings {
25 show_arrival: bool,
26 show_departure: bool,
27 show_line_above: bool,
28 show_line_below: bool,
29}
30
31impl Default for StationSettings {
32 fn default() -> Self {
33 Self {
34 show_arrival: false,
35 show_departure: true,
36 show_line_above: false,
37 show_line_below: false,
38 }
39 }
40}
41
42#[derive(Serialize, Clone, Copy, PartialEq)]
43#[serde(rename_all = "kebab-case")]
44enum OperateMode {
45 NoOperation,
48 Skip,
51 NonStop(i32),
54 Stop(i32, i32),
55}
56
57#[derive(Serialize)]
58#[serde(rename_all = "kebab-case")]
59struct ExportedVehicle {
60 name: String,
61 schedule: Vec<OperateMode>,
62}
63
64impl<I: Iterator<Item = Entity>> super::ExportObject<(I, Entity)> for TypstTimetable {
65 fn extension(&self) -> impl AsRef<str> {
66 ".json"
67 }
68 fn filename(&self) -> impl AsRef<str> {
69 "exported_timetable"
70 }
71 fn export_to_buffer(
72 &mut self,
73 world: &mut World,
74 buffer: &mut Vec<u8>,
75 input: (I, Entity),
76 ) -> Result<(), Box<dyn std::error::Error>> {
77 let (vehicle_entities, displayed_line_entity) = input;
79 let Some(displayed_line) = world.get::<DisplayedLine>(displayed_line_entity) else {
80 return Err("Displayed line entity does not have a DisplayedLine component".into());
81 };
82 let mut root = Root::default();
83 let station_list = displayed_line
84 .stations
85 .iter()
86 .map(|(station, _)| *station)
87 .collect::<Vec<_>>();
88 root.stations.extend(station_list.iter().map(|s| {
89 let name = world
90 .get::<Name>(s.entity())
91 .map_or("<unnamed>".into(), Name::to_string);
92 (name, StationSettings::default())
93 }));
94 for entity in vehicle_entities {
95 let mut vehicle = ExportedVehicle {
96 name: String::new(),
97 schedule: vec![OperateMode::Skip; station_list.len()],
98 };
99 if let Err(e) = world.run_system_once_with(
100 make_json,
101 (
102 &mut vehicle.name,
103 &mut vehicle.schedule,
104 entity,
105 &station_list,
106 ),
107 ) {
108 };
110 root.vehicles.push(vehicle);
111 }
112 serde_json::to_writer_pretty(buffer, &root)?;
113 Ok(())
114 }
115}
116
117fn make_json(
118 (InMut(vehicle_name), InMut(vehicle_schedule), In(entity), InRef(station_list)): (
119 InMut<String>,
120 InMut<[OperateMode]>,
121 In<Entity>,
122 InRef<[Instance<Station>]>,
123 ),
124 timetable_entries: Query<(&TimetableEntry, &TimetableEntryCache)>,
125 vehicles: Query<(&Name, &VehicleSchedule, &VehicleScheduleCache)>,
126 graph: Res<Graph>,
127) {
128 let Ok((name, schedule, schedule_cache)) = vehicles.get(entity) else {
129 return;
130 };
131 *vehicle_name = name.to_string();
132 for (entry, entry_cache) in schedule_cache
133 .actual_route
134 .as_deref()
135 .into_iter()
136 .flatten()
137 .filter_map(|e| timetable_entries.get(e.inner()).ok())
138 {
139 let Some(time) = entry_cache.estimate.as_ref() else {
140 continue;
141 };
142 for i in station_list.iter().positions(|s| *s == entry.station()) {
143 if entry.departure.is_none() {
144 vehicle_schedule[i] = OperateMode::NonStop(time.departure.0)
145 } else {
146 vehicle_schedule[i] = OperateMode::Stop(time.arrival.0, time.departure.0)
147 }
148 }
149 }
150 for curr in 0..vehicle_schedule.len() {
151 let current_station = station_list[curr];
152 let prev_is_connected = curr == 0
153 || station_list
154 .get(curr - 1)
155 .map_or(false, |s| graph.contains_edge(*s, current_station));
156 let next_is_connected = station_list
157 .get(curr + 1)
158 .map_or(false, |s| graph.contains_edge(current_station, *s));
159 let prev_is_continuous = curr == 0
160 || vehicle_schedule
161 .get(curr - 1)
162 .map_or(false, |om| *om != OperateMode::Skip);
163 let next_is_continuous = vehicle_schedule
164 .get(curr + 1)
165 .map_or(false, |om| *om != OperateMode::Skip);
166 let invalid = (!prev_is_continuous && !next_is_continuous)
167 || (!prev_is_connected && prev_is_continuous && !next_is_continuous)
168 || (!next_is_connected && next_is_continuous && !prev_is_continuous);
169 if invalid {
170 vehicle_schedule[curr] = OperateMode::Skip
171 }
172 }
173 if let Some(i1) = vehicle_schedule
174 .iter()
175 .position(|om| !matches!(om, OperateMode::Skip))
176 {
177 for om in vehicle_schedule[..i1].iter_mut() {
178 *om = OperateMode::NoOperation
179 }
180 };
181 if let Some(i2) = vehicle_schedule
182 .iter()
183 .rposition(|om| !matches!(om, OperateMode::Skip))
184 {
185 if i2 + 1 < vehicle_schedule.len() {
186 for om in vehicle_schedule[i2 + 1..].iter_mut() {
187 *om = OperateMode::NoOperation
188 }
189 }
190 }
191}