1use std::path::PathBuf;
5
6use bevy::platform::collections::HashMap;
7use bevy::prelude::*;
8use bevy::tasks::futures_lite::future::poll_once;
9use bevy::tasks::{AsyncComputeTaskPool, Task, block_on};
10use eros::bail;
11use moonshine_core::kind::*;
12use paiagram_rw::save::{LoadCandidate, SaveData};
13
14use crate::graph::Graph;
15use crate::interval::Interval;
16use crate::station::Station;
17use crate::trip::class::{Class, ClassBundle};
18use crate::units::distance::Distance;
19use crate::units::time::{Duration, TimetableTime};
20
21mod gtfs;
22mod llt;
23mod oudia;
24mod qetrc;
25
26pub struct ImportPlugin;
27impl Plugin for ImportPlugin {
28 fn build(&self, app: &mut App) {
29 app.add_observer(qetrc::load_qetrc)
30 .add_observer(oudia::load_oud)
31 .add_observer(gtfs::load_gtfs_static)
32 .add_observer(llt::load_llt)
33 .add_observer(download_file)
34 .add_systems(Update, pull_file);
35 }
36}
37
38#[derive(Event)]
39pub struct LoadQETRC {
40 pub content: String,
41}
42
43#[derive(Event)]
44pub struct LoadLlt {
45 pub content: String,
46}
47
48pub enum OuDiaContentType {
49 OuDiaSecond(String),
50 OuDia(Vec<u8>),
51}
52
53#[derive(Event)]
54pub struct LoadOuDia {
55 pub content: OuDiaContentType,
56}
57
58impl LoadOuDia {
59 pub fn original(data: Vec<u8>) -> Self {
60 Self {
61 content: OuDiaContentType::OuDia(data),
62 }
63 }
64 pub fn second(data: String) -> Self {
65 Self {
66 content: OuDiaContentType::OuDiaSecond(data),
67 }
68 }
69}
70
71#[derive(Event)]
72pub struct LoadGTFS {
73 pub content: Vec<u8>,
74}
75
76#[derive(Event)]
77pub struct DownloadFile {
78 pub url: String,
79}
80
81fn normalize_times<'a>(mut time_iter: impl Iterator<Item = &'a mut TimetableTime> + 'a) {
82 let Some(mut previous_time) = time_iter.next().copied() else {
83 return;
84 };
85 for time in time_iter {
86 while *time < previous_time {
87 *time += Duration(86400);
88 }
89 previous_time = *time;
90 }
91}
92
93pub(crate) fn make_station(
94 name: &str,
95 station_map: &mut HashMap<String, Instance<Station>>,
96 graph: &mut Graph,
97 commands: &mut Commands,
98) -> Instance<Station> {
99 if let Some(&entity) = station_map.get(name) {
100 return entity;
101 }
102 let station_entity = commands
103 .spawn(Name::new(name.to_string()))
104 .insert_instance(Station::default())
105 .into();
106 station_map.insert(name.to_string(), station_entity);
107 graph.add_node(station_entity.entity());
108 station_entity
109}
110
111pub(crate) fn make_class(
112 name: &str,
113 class_map: &mut HashMap<String, Instance<Class>>,
114 commands: &mut Commands,
115 mut make_class: impl FnMut() -> ClassBundle,
116) -> Instance<Class> {
117 if let Some(&entity) = class_map.get(name) {
118 return entity;
119 };
120 let class_bundle = make_class();
121 let class_entity = commands
122 .spawn((class_bundle.name, class_bundle.stroke))
123 .insert_instance(class_bundle.class)
124 .into();
125 class_map.insert(name.to_string(), class_entity);
126 class_entity
127}
128
129pub(crate) fn add_interval_pair(
131 graph: &mut Graph,
132 commands: &mut Commands,
133 from: Entity,
134 to: Entity,
135 length: Distance,
136) {
137 if !graph.contains_edge(from, to) {
138 let e1: Instance<Interval> = commands.spawn_instance(Interval { length }).into();
139 graph.add_edge(from, to, e1.entity());
140 }
141 if !graph.contains_edge(to, from) {
142 let e2: Instance<Interval> = commands.spawn_instance(Interval { length }).into();
143 graph.add_edge(to, from, e2.entity());
144 }
145}
146
147#[derive(Component)]
148pub struct FileDownloadTask {
149 task: Option<Task<(Vec<u8>, String)>>,
150 url: String,
151}
152
153pub fn download_file(event: On<DownloadFile>, mut commands: Commands) {
154 commands.spawn(FileDownloadTask {
155 task: None,
156 url: event.url.clone(),
157 });
158}
159
160fn pull_file(mut commands: Commands, tasks: Populated<(Entity, &mut FileDownloadTask)>) {
161 for (task_entity, mut task) in tasks {
162 if task.task.is_none() {
163 let url = task.url.clone();
164 task.task = Some(AsyncComputeTaskPool::get().spawn(async move {
165 let response = ehttp::fetch_async(ehttp::Request::get(&url))
166 .await
167 .unwrap_or_else(|e| panic!("Failed to download file from {url}: {e:?}"));
168 if !response.ok {
169 panic!(
170 "Failed to download file from {url}: status={} {}",
171 response.status, response.status_text
172 );
173 }
174 (response.bytes, response.url)
175 }));
176 continue;
177 }
178
179 let Some(task_handle) = task.task.as_mut() else {
180 continue;
181 };
182 let Some((content, final_url)) = block_on(poll_once(task_handle)) else {
183 continue;
184 };
185
186 let path = infer_path_from_url(&final_url)
187 .or_else(|| infer_path_from_url(&task.url))
188 .unwrap_or_else(|| PathBuf::from(task.url.clone()));
189 if let Err(e) = load_and_trigger(&path, content, &mut commands) {
190 error!(
191 "Failed to load downloaded file from {} (resolved as {}): {e:#}",
192 task.url,
193 path.display(),
194 );
195 }
196 commands.entity(task_entity).despawn();
197 }
198}
199
200fn infer_path_from_url(url: &str) -> Option<PathBuf> {
201 let no_query = url.split('?').next().unwrap_or(url);
202 let no_fragment = no_query.split('#').next().unwrap_or(no_query);
203 let filename = no_fragment.rsplit('/').next().unwrap_or_default().trim();
204 if filename.is_empty() {
205 return None;
206 }
207 Some(PathBuf::from(filename))
208}
209
210pub fn load_and_trigger(
211 path: &PathBuf,
212 content: Vec<u8>,
213 commands: &mut Commands,
214) -> eros::Result<()> {
215 match path.extension().and_then(|s| s.to_str()) {
216 Some("paia") => {
217 commands.insert_resource(LoadCandidate(SaveData::CompressedCbor(content)));
218 }
219 Some("pyetgr") | Some("json") => {
220 let content = String::from_utf8(content)?;
221 commands.trigger(LoadQETRC { content });
222 }
223 Some("oud2") => {
224 let content = String::from_utf8(content)?;
225 commands.trigger(LoadOuDia::second(content));
226 }
227 Some("zip") => {
228 commands.trigger(LoadGTFS { content });
229 }
230 Some("oud") => {
231 commands.trigger(LoadOuDia::original(content))
233 }
234 Some("ron") => {
235 commands.insert_resource(LoadCandidate(SaveData::Ron(content)));
236 }
237 Some(e) => bail!("Unexpected extension: {}", e),
238 None => bail!("Path does not have an extension"),
239 }
240 return Ok(());
241}