paiagram/
main.rs

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        // set up bevy world
32        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        // don't load autosave if opening a file or starting fresh
51        let mut load_autosave = true;
52        // get the world's settings resource to get the language
53        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        // this is true regardless of settings, as we always want to persist egui memory
91        // autosave is handled separately
92        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        // a dummy marker for the storage system
104        // this saves stuff interval to egui e.g. window positions etc.
105        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        // save the app state
115        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 the ending of the path
153        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// When compiling to web using trunk:
212#[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            // Set styles to ensure full screen and correct rendering
237            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        // Remove the loading text and spinner:
270        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}