paiagram/interface/tabs/
vehicle.rs

1use crate::interface::UiCommand;
2use crate::status_bar_text::SetStatusBarText;
3use crate::{
4    search::{SearchCommand, SearchResponse},
5    vehicles::{Schedule, TimetableEntry},
6};
7use bevy::ecs::message::MessageId;
8use bevy::prelude::*;
9use bevy_egui::egui;
10use egui_deferred_table::{
11    AxisParameters, CellIndex, DeferredTable, DeferredTableDataSource, DeferredTableRenderer,
12    TableDimensions,
13};
14use std::{collections::HashMap, sync::Arc};
15
16#[derive(Clone)]
17struct VehicleScheduleRow {
18    station_name: String,
19    arrival: String,
20    departure: String,
21    service: String,
22    track: String,
23}
24
25impl VehicleScheduleRow {
26    fn all_fields(&self) -> String {
27        format!(
28            "{} {} {} {} {}",
29            self.station_name, self.arrival, self.departure, self.service, self.track
30        )
31    }
32}
33
34#[derive(Default)]
35struct VehicleScheduleDataSource {
36    rows: Vec<VehicleScheduleRow>,
37}
38
39impl VehicleScheduleDataSource {
40    fn clear(&mut self) {
41        self.rows.clear();
42    }
43
44    fn push(&mut self, row: VehicleScheduleRow) {
45        self.rows.push(row);
46    }
47
48    fn row(&self, index: usize) -> Option<&VehicleScheduleRow> {
49        self.rows.get(index)
50    }
51
52    fn iter(&self) -> std::slice::Iter<'_, VehicleScheduleRow> {
53        self.rows.iter()
54    }
55}
56
57impl DeferredTableDataSource for VehicleScheduleDataSource {
58    fn get_dimensions(&self) -> TableDimensions {
59        TableDimensions {
60            row_count: self.rows.len(),
61            column_count: 4,
62        }
63    }
64}
65
66#[derive(Default)]
67pub struct VehicleScheduleCache {
68    data_source: VehicleScheduleDataSource,
69}
70
71impl VehicleScheduleCache {
72    fn refresh(
73        &mut self,
74        schedule: &Schedule,
75        entries: &Query<(&TimetableEntry, &ChildOf)>,
76        names: &Query<&Name>,
77        entity: Entity,
78    ) {
79        self.data_source.clear();
80
81        let expected_rows = schedule.1.len();
82        if expected_rows > 0 {
83            self.data_source.rows.reserve(expected_rows);
84        }
85
86        for entry_entity in &schedule.1 {
87            let Ok((entry, parent)) = entries.get(*entry_entity) else {
88                continue;
89            };
90
91            let station_name = names
92                .get(entry.station)
93                .map(|name| name.as_str().to_owned())
94                .unwrap_or_else(|_| "???".to_string());
95
96            let service = entry
97                .service
98                .and_then(|service_entity| {
99                    names
100                        .get(service_entity)
101                        .ok()
102                        .map(|n| n.as_str().to_owned())
103                })
104                .unwrap_or_else(|| "—".to_string());
105
106            let track = entry
107                .track
108                .map(|track_entity| format!("{track_entity:?}"))
109                .unwrap_or_else(|| "—".to_string());
110
111            self.data_source.push(VehicleScheduleRow {
112                station_name,
113                arrival: entry.arrival.to_string(),
114                departure: entry.departure.to_string(),
115                service,
116                track,
117            });
118        }
119    }
120}
121
122struct VehicleScheduleRenderer<'a> {
123    filtered_rows: &'a [usize],
124}
125
126impl<'a> DeferredTableRenderer<VehicleScheduleDataSource> for VehicleScheduleRenderer<'a> {
127    fn render_cell(
128        &self,
129        ui: &mut bevy_egui::egui::Ui,
130        cell_index: CellIndex,
131        source: &VehicleScheduleDataSource,
132    ) {
133        let Some(row) = source.row(cell_index.row) else {
134            return;
135        };
136
137        let label = match cell_index.column {
138            0 => &row.arrival,
139            1 => &row.departure,
140            2 => &row.service,
141            3 => &row.track,
142            _ => return,
143        };
144        ui.monospace(label);
145    }
146
147    fn rows_to_filter(&self) -> Option<&[usize]> {
148        Some(self.filtered_rows)
149    }
150}
151
152const FILTER_THROTTLE: f32 = 0.01;
153
154#[derive(Default)]
155pub(crate) struct VehicleDisplayCache {
156    query: String,
157    filtered_rows: Vec<usize>,
158    last_schedule_len: usize,
159    dirty: bool,
160    last_request_time: Option<f32>,
161    in_flight: Option<MessageId<SearchCommand>>,
162}
163
164impl VehicleDisplayCache {
165    fn query_mut(&mut self) -> &mut String {
166        &mut self.query
167    }
168
169    fn on_query_changed(&mut self) {
170        self.dirty = true;
171        if self.query.is_empty() {
172            self.filtered_rows.clear();
173        }
174    }
175
176    fn update_schedule_len(&mut self, len: usize) {
177        if self.last_schedule_len != len {
178            self.last_schedule_len = len;
179            self.dirty = true;
180        }
181    }
182
183    fn filtered_rows<'a>(
184        &'a mut self,
185        entity: Entity,
186        now_seconds: f32,
187        data_source: &VehicleScheduleDataSource,
188        search_writer: &mut MessageWriter<SearchCommand>,
189        pending_requests: &mut HashMap<MessageId<SearchCommand>, Entity>,
190    ) -> &'a [usize] {
191        if self.query.trim().is_empty() {
192            if !self.filtered_rows.is_empty() {
193                self.filtered_rows.clear();
194            }
195            if let Some(previous) = self.in_flight.take() {
196                pending_requests.remove(&previous);
197            }
198            self.dirty = false;
199            return &self.filtered_rows;
200        }
201
202        let throttle_ok = self
203            .last_request_time
204            .map_or(true, |last| now_seconds - last >= FILTER_THROTTLE);
205        let must_dispatch =
206            self.dirty || (self.in_flight.is_none() && self.filtered_rows.is_empty());
207
208        if must_dispatch && throttle_ok {
209            if let Some(previous) = self.in_flight.take() {
210                pending_requests.remove(&previous);
211            }
212
213            let payload: Vec<String> = data_source.iter().map(|row| row.all_fields()).collect();
214
215            let command_id = search_writer.write(SearchCommand::Table {
216                data: Arc::new(payload),
217                query: self.query.clone(),
218            });
219
220            pending_requests.insert(command_id, entity);
221            self.in_flight = Some(command_id);
222            self.last_request_time = Some(now_seconds);
223            self.dirty = false;
224        }
225
226        &self.filtered_rows
227    }
228}
229
230pub fn show_vehicle(
231    (InMut(ui), In(entity)): (InMut<egui::Ui>, In<Entity>),
232    mut cache: Local<VehicleScheduleCache>,
233    mut display_cache: Local<HashMap<Entity, VehicleDisplayCache>>,
234    mut pending_requests: Local<HashMap<MessageId<SearchCommand>, Entity>>,
235    mut search_responses: MessageReader<SearchResponse>,
236    mut search_writer: MessageWriter<SearchCommand>,
237    mut ui_command_writer: MessageWriter<UiCommand>,
238    time: Res<Time>,
239    schedules: Query<&Schedule>,
240    entries: Query<(&TimetableEntry, &ChildOf)>,
241    names: Query<&Name>,
242) {
243    let Some(schedule) = schedules.get(entity).ok() else {
244        ui.label("No schedule found");
245        return;
246    };
247    cache.refresh(schedule, &entries, &names, entity);
248
249    let now_seconds = time.elapsed_secs();
250
251    for response in search_responses.read() {
252        match response {
253            SearchResponse::Table(command_id, rows) => {
254                if let Some(target_entity) = pending_requests.remove(command_id) {
255                    if let Some(entry_cache) = display_cache.get_mut(&target_entity) {
256                        if entry_cache.in_flight == Some(*command_id) {
257                            entry_cache.filtered_rows = rows.clone();
258                            entry_cache.in_flight = None;
259                            entry_cache.last_request_time = Some(now_seconds);
260                        }
261                    }
262                }
263            }
264        }
265    }
266
267    let cache_entry = display_cache.entry(entity).or_default();
268    cache_entry.update_schedule_len(cache.data_source.rows.len());
269
270    // search box
271    ui.horizontal(|ui| {
272        ui.label("Search:");
273        let search_input = ui
274            .text_edit_singleline(cache_entry.query_mut())
275            .set_status_bar_text("🔍 Search entries", &mut ui_command_writer);
276        if search_input.changed() {
277            cache_entry.on_query_changed();
278        }
279    });
280
281    if cache.data_source.rows.is_empty() {
282        ui.label("No timetable entries for this vehicle.");
283        return;
284    }
285
286    let row_parameters: Vec<AxisParameters> = cache
287        .data_source
288        .iter()
289        .map(|row| {
290            AxisParameters::default()
291                .name(&row.station_name)
292                .default_dimension(14.0)
293                .resizable(false)
294        })
295        .collect();
296
297    let column_parameters = vec![
298        AxisParameters::default()
299            .name("Arrival")
300            .default_dimension(70.0)
301            .resizable(false),
302        AxisParameters::default()
303            .name("Departure")
304            .default_dimension(70.0)
305            .resizable(false),
306        AxisParameters::default()
307            .name("Service")
308            .default_dimension(70.0)
309            .resizable(false),
310        AxisParameters::default()
311            .name("Track")
312            .default_dimension(70.0)
313            .resizable(false),
314    ];
315
316    let mut table = DeferredTable::new(ui.id().with("vehicle_schedule"))
317        .default_cell_size(egui::vec2(90.0, 14.0))
318        .selectable_rows_disabled()
319        .highlight_hovered_cell();
320
321    table = table.column_parameters(&column_parameters);
322
323    if !row_parameters.is_empty() {
324        table = table.row_parameters(&row_parameters);
325    }
326
327    let filtered_rows = cache_entry.filtered_rows(
328        entity,
329        now_seconds,
330        &cache.data_source,
331        &mut search_writer,
332        &mut pending_requests,
333    );
334
335    let data_source = &mut cache.data_source;
336    let mut renderer = VehicleScheduleRenderer { filtered_rows };
337    let (_response, _actions) = table.show(ui, data_source, &mut renderer);
338}