paiagram/export/
typst_timetable.rs

1use 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    /// The vehicle does not operate at this stop at all.
46    /// Equivalent to "..." in Japanese timetables
47    NoOperation,
48    /// The vehicle does not operate at this stop at all.
49    /// Equivalent to "||"
50    Skip,
51    /// The vehicle bypasses this stop
52    /// Equivalent to "re"
53    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        // TODO: Handle the return values here
78        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                // TODO
109            };
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}