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 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}