1use bevy::{ecs::system::RunSystemOnce, log::LogPlugin, prelude::*};
2#[cfg(not(target_arch = "wasm32"))]
3use clap::Parser;
4use moonshine_core::{load::load_on_default_event, save::save_on_default_event};
5
6mod colors;
7mod export;
8mod graph;
9mod i18n;
10mod interface;
11mod lines;
12mod rw_data;
13mod search;
14mod settings;
15mod status_bar_text;
16mod troubleshoot;
17mod units;
18mod vehicles;
19
20struct PaiagramApp {
21 bevy_app: App,
22}
23
24impl PaiagramApp {
25 fn new(cc: &eframe::CreationContext) -> Self {
26 cc.egui_ctx.style_mut(|style| {
27 style.spacing.window_margin = egui::Margin::same(2);
28 style.interaction.selectable_labels = false;
29 });
30 interface::apply_custom_fonts(&cc.egui_ctx);
31 let mut app = App::new();
33 app.add_plugins(MinimalPlugins);
34 app.add_plugins(LogPlugin::default());
35 app.add_plugins((
36 interface::InterfacePlugin,
37 graph::GraphPlugin,
38 rw_data::RwDataPlugin,
39 search::SearchPlugin,
40 settings::SettingsPlugin,
41 vehicles::VehiclesPlugin,
42 lines::LinesPlugin,
43 troubleshoot::TroubleShootPlugin,
44 ))
45 .add_observer(save_on_default_event)
46 .add_observer(load_on_default_event);
47 #[cfg(target_arch = "wasm32")]
48 app.add_observer(rw_data::saveload::observe_autosave);
49 info!("Initialized Bevy App.");
50 let mut load_autosave = true;
52 let settings = app.world().resource::<settings::ApplicationSettings>();
54 i18n::init(Some(settings.language.identifier()));
55 #[cfg(not(target_arch = "wasm32"))]
56 {
57 let args = Cli::parse();
58 if args.open.is_some() || args.fresh {
59 load_autosave = false;
60 }
61 if let Err(e) = app.world_mut().run_system_once_with(handle_args, args) {
62 error!("Failed to handle command line arguments: {:?}", e);
63 } else {
64 info!("Command line arguments handled successfully.");
65 }
66 }
67 if load_autosave
68 && let Err(e) = app
69 .world_mut()
70 .run_system_once(rw_data::saveload::load_autosave)
71 {
72 error!("Failed to load autosave: {:?}", e);
73 }
74 Self { bevy_app: app }
75 }
76}
77
78impl eframe::App for PaiagramApp {
79 fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
80 self.bevy_app
81 .world_mut()
82 .resource_mut::<interface::MiscUiState>()
83 .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
84 self.bevy_app.update();
85 if let Err(e) = interface::show_ui(self, ctx) {
86 error!("UI Error: {:?}", e);
87 }
88 }
89 fn persist_egui_memory(&self) -> bool {
90 true
93 }
94 fn auto_save_interval(&self) -> std::time::Duration {
95 let mins = self
96 .bevy_app
97 .world()
98 .resource::<settings::ApplicationSettings>()
99 .autosave_interval_minutes;
100 std::time::Duration::from_mins(mins as u64)
101 }
102 fn save(&mut self, storage: &mut dyn eframe::Storage) {
103 eframe::set_value(storage, "autosave_marker", &());
106 let autosave_enabled = self
107 .bevy_app
108 .world()
109 .resource::<settings::ApplicationSettings>()
110 .autosave_enabled;
111 if !autosave_enabled {
112 return;
113 }
114 if let Err(e) = self
116 .bevy_app
117 .world_mut()
118 .run_system_once(rw_data::saveload::autosave)
119 {
120 error!("Autosave failed: {:?}", e);
121 }
122 }
123}
124
125#[derive(Parser)]
126#[command(version, about, long_about = None)]
127#[cfg(not(target_arch = "wasm32"))]
128struct Cli {
129 #[arg(
130 short = 'o',
131 long = "open",
132 help = "Path to a .paiagram file (or any other compatible file formats) to open on startup"
133 )]
134 open: Option<String>,
135 #[arg(
136 long = "fresh",
137 help = "Start with a fresh state, ignoring any autosave"
138 )]
139 fresh: bool,
140 #[arg(
141 long = "jgrpp",
142 help = "Path to a set of OpenTTD JGRPP .json timetable export files. You may specify multiple by using the * syntax (usually this is expanded by your shell). Example: --jgrpp ~/.local/share/openttd/orderlist/*.json",
143 num_args = 1..
144 )]
145 jgrpp_paths: Option<Vec<String>>,
146}
147
148#[cfg(not(target_arch = "wasm32"))]
149fn handle_args(cli: In<Cli>, mut msg: MessageWriter<rw_data::ModifyData>, mut commands: Commands) {
150 use rw_data::ModifyData;
151 if let Some(path) = &cli.open {
152 match path.split('.').next_back() {
154 Some("paiagram") => {
155 rw_data::saveload::load_save(&mut commands, path.into());
156 }
157 Some("json") | Some("pyetgr") => {
158 let file_content = std::fs::read_to_string(path).expect("Failed to read file");
159 msg.write(ModifyData::LoadQETRC(file_content));
160 }
161 Some("oud2") => {
162 let file_content = std::fs::read_to_string(path).expect("Failed to read file");
163 msg.write(ModifyData::LoadOuDiaSecond(file_content));
164 }
165 Some("zip") => {
166 let file_content = std::fs::read(path).expect("Failed to read file");
167 commands.trigger(rw_data::gtfs::GtfsLoaded(file_content));
168 }
169 _ => {
170 warn!("Unsupported file format: {}", path);
171 }
172 }
173 return;
174 }
175 if let Some(paths) = &cli.jgrpp_paths {
176 let mut contents = Vec::with_capacity(paths.len());
177 for path in paths {
178 contents.push(std::fs::read_to_string(path).expect("Failed to read file"));
179 }
180 msg.write(ModifyData::LoadJGRPP(contents));
181 }
182}
183
184#[cfg(not(target_arch = "wasm32"))]
185fn main() -> eframe::Result<()> {
186 let native_options = eframe::NativeOptions {
187 viewport: egui::ViewportBuilder::default()
188 .with_title("Drawer")
189 .with_app_id("Paiagram")
190 .with_inner_size([1280.0, 720.0]),
191 ..default()
192 };
193
194 eframe::run_native(
195 "Paiagram Drawer",
196 native_options,
197 Box::new(|cc| Ok(Box::new(PaiagramApp::new(cc)))),
198 )
199}
200
201#[cfg(target_arch = "wasm32")]
202use wasm_bindgen::prelude::*;
203
204#[cfg(target_arch = "wasm32")]
205#[derive(Clone)]
206#[wasm_bindgen]
207pub struct WebHandle {
208 runner: eframe::WebRunner,
209}
210
211#[cfg(target_arch = "wasm32")]
213fn main() {
214 i18n::init(None);
215 use eframe::wasm_bindgen::JsCast as _;
216 use eframe::web_sys;
217
218 let web_options = eframe::WebOptions::default();
219
220 wasm_bindgen_futures::spawn_local(async {
221 let document = web_sys::window()
222 .expect("No window")
223 .document()
224 .expect("No document");
225
226 let canvas = if let Some(canvas) = document.get_element_by_id("paiagram_canvas") {
227 canvas
228 .dyn_into::<web_sys::HtmlCanvasElement>()
229 .expect("paiagram_canvas was not a HtmlCanvasElement")
230 } else {
231 let canvas = document
232 .create_element("canvas")
233 .expect("Failed to create canvas element");
234 canvas.set_id("paiagram_canvas");
235
236 canvas
238 .set_attribute("style", "display: block; width: 100%; height: 100%;")
239 .ok();
240
241 let body = document.body().expect("Failed to get document body");
242 body.set_attribute(
243 "style",
244 "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden;",
245 )
246 .ok();
247
248 let html = document.document_element().expect("No document element");
249 html.set_attribute(
250 "style",
251 "margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden;",
252 )
253 .ok();
254
255 body.append_child(&canvas).expect("Failed to append canvas");
256 canvas
257 .dyn_into::<web_sys::HtmlCanvasElement>()
258 .expect("Failed to cast canvas")
259 };
260
261 let start_result = eframe::WebRunner::new()
262 .start(
263 canvas,
264 web_options,
265 Box::new(|cc| Ok(Box::new(PaiagramApp::new(cc)))),
266 )
267 .await;
268
269 if let Some(loading_text) = document.get_element_by_id("loading_text") {
271 match start_result {
272 Ok(_) => {
273 loading_text.remove();
274 }
275 Err(e) => {
276 loading_text.set_inner_html(
277 "<p> The app has crashed. See the developer console for details. </p>",
278 );
279 panic!("Failed to start eframe: {e:?}");
280 }
281 }
282 }
283 });
284}