paiagram/interface/tabs/diagram/
calculate_lines.rs

1use bevy::prelude::*;
2use egui::Pos2;
3
4use crate::graph::{Station, StationEntries};
5use crate::lines::DisplayedLine;
6use crate::units::time::TimetableTime;
7use crate::vehicles::entries::{
8    TimetableEntry, TimetableEntryCache, VehicleSchedule, VehicleScheduleCache,
9};
10use crate::vehicles::vehicle_set::VehicleSet;
11
12use super::{
13    DiagramLineCache, DiagramLineParams, PointData, RenderedVehicle, TICKS_PER_SECOND,
14    ensure_heights, ticks_to_screen_x,
15};
16
17pub fn calculate_lines(
18    (In(displayed_line_entity), InMut(mut line_cache), InMut(buffer), In(line_params)): (
19        In<Entity>,
20        InMut<DiagramLineCache>,
21        InMut<Vec<RenderedVehicle>>,
22        In<DiagramLineParams>,
23    ),
24    displayed_lines: Populated<Ref<DisplayedLine>>,
25    vehicles_query: Populated<(Entity, &Name, &VehicleSchedule, &VehicleScheduleCache)>,
26    entry_parents: Query<&ChildOf, With<TimetableEntry>>,
27    timetable_entries: Query<(&TimetableEntry, &TimetableEntryCache)>,
28    station_updated: Query<&StationEntries, Changed<StationEntries>>,
29    station_caches: Query<&StationEntries, With<Station>>,
30    vehicle_sets: Query<&Children, With<VehicleSet>>,
31    mut previous_vehicle_set: Local<Option<Entity>>,
32) {
33    let Ok(displayed_line) = displayed_lines.get(displayed_line_entity) else {
34        line_cache.line_missing = true;
35        buffer.clear();
36        return;
37    };
38    line_cache.line_missing = false;
39
40    let entries_updated = displayed_line.is_changed()
41        || line_cache.vehicle_entities.is_empty()
42        || displayed_line
43            .stations()
44            .iter()
45            .copied()
46            .any(|(s, _)| station_updated.get(s.entity()).is_ok())
47        || previous_vehicle_set.as_ref() != line_cache.vehicle_set.as_ref();
48
49    if entries_updated {
50        info!("Updating vehicle entities for diagram display");
51
52        for station in
53            station_caches.iter_many(displayed_line.stations().iter().map(|(e, _)| e.entity()))
54        {
55            station.passing_vehicles(&mut line_cache.vehicle_entities, |e| {
56                entry_parents.get(e).ok()
57            });
58        }
59        line_cache.vehicle_entities.sort();
60        // filter out those not in the vehicle set, if any
61        if let Some(vehicle_set_entity) = line_cache.vehicle_set {
62            if let Ok(vehicle_set) = vehicle_sets.get(vehicle_set_entity) {
63                line_cache
64                    .vehicle_entities
65                    .retain(|e| vehicle_set.contains(e));
66            }
67        }
68        line_cache.vehicle_entities.dedup();
69        *previous_vehicle_set = line_cache.vehicle_set;
70    }
71
72    if displayed_line.is_changed() || line_cache.heights.is_none() {
73        ensure_heights(&mut line_cache, &displayed_line);
74    }
75
76    let Some(render_context) = line_cache.last_render_context.clone() else {
77        buffer.clear();
78        return;
79    };
80
81    let visible_stations = match line_cache.heights.as_ref() {
82        Some(heights) => {
83            let first_visible = heights
84                .iter()
85                .position(|(_, h)| *h > render_context.vertical_visible.start);
86            let last_visible = heights
87                .iter()
88                .rposition(|(_, h)| *h < render_context.vertical_visible.end);
89            if let (Some(mut first_visible), Some(mut last_visible)) = (first_visible, last_visible)
90            {
91                first_visible = first_visible.saturating_sub(2);
92                last_visible = (last_visible + 1).min(heights.len() - 1);
93                &heights[first_visible..=last_visible]
94            } else {
95                &[]
96            }
97        }
98        None => &[],
99    };
100
101    let mut rendered_vehicles = Vec::new();
102
103    for (vehicle_entity, _name, schedule, schedule_cache) in line_cache
104        .vehicle_entities
105        .iter()
106        .copied()
107        .filter_map(|e| vehicles_query.get(e).ok())
108    {
109        // Get all repetitions of the schedule that fall within the visible time range.
110        let Some(visible_sets) = schedule_cache.get_entries_range(
111            schedule,
112            TimetableTime((render_context.horizontal_visible.start / TICKS_PER_SECOND) as i32)
113                ..TimetableTime((render_context.horizontal_visible.end / TICKS_PER_SECOND) as i32),
114            |e| timetable_entries.get(e).ok(),
115        ) else {
116            continue;
117        };
118
119        let mut segments = Vec::new();
120
121        for (initial_offset, set) in visible_sets {
122            // local_edges holds WIP segments and the index of the station line they are currently on.
123            let mut local_edges: Vec<(Vec<PointData>, usize)> = Vec::new();
124            let mut previous_indices: Vec<usize> = Vec::new();
125
126            // Initialize previous_indices with all occurrences of the first station in the set.
127            if let Some((ce_data, _)) = set.first() {
128                let (ce, _) = ce_data;
129                previous_indices = visible_stations
130                    .iter()
131                    .enumerate()
132                    .filter_map(|(i, (s, _))| if *s == ce.station { Some(i) } else { None })
133                    .collect();
134            }
135
136            for entry_idx in 0..set.len() {
137                let (ce_data, ce_actual) = &set[entry_idx];
138                let (ce, ce_cache) = ce_data;
139                let ne = set.get(entry_idx + 1);
140
141                // If the current station isn't visible, try to find the next one and flush WIP edges.
142                if previous_indices.is_empty() {
143                    if let Some((ne_data, _)) = ne {
144                        let (ne_entry, _) = ne_data;
145                        previous_indices = visible_stations
146                            .iter()
147                            .enumerate()
148                            .filter_map(|(i, (s, _))| {
149                                if *s == ne_entry.station {
150                                    Some(i)
151                                } else {
152                                    None
153                                }
154                            })
155                            .collect();
156                    }
157                    for (segment, _) in local_edges.drain(..) {
158                        if segment.len() >= 2 {
159                            segments.push(segment);
160                        }
161                    }
162                    continue;
163                }
164
165                let mut next_local_edges = Vec::new();
166
167                // If there's no time estimate, we can't draw this point. Flush WIP edges.
168                let Some(estimate) = ce_cache.estimate.as_ref() else {
169                    for (segment, _) in local_edges.drain(..) {
170                        if segment.len() >= 2 {
171                            segments.push(segment);
172                        }
173                    }
174                    if let Some((ne_data, _)) = ne {
175                        let (ne_entry, _) = ne_data;
176                        previous_indices = visible_stations
177                            .iter()
178                            .enumerate()
179                            .filter_map(|(i, (s, _))| {
180                                if *s == ne_entry.station {
181                                    Some(i)
182                                } else {
183                                    None
184                                }
185                            })
186                            .collect();
187                    }
188                    continue;
189                };
190
191                // Calculate absolute ticks for arrival and departure.
192                let arrival_ticks = (initial_offset.0 as i64
193                    + (estimate.arrival.0 - schedule.start.0) as i64)
194                    * TICKS_PER_SECOND;
195                let departure_ticks = (initial_offset.0 as i64
196                    + (estimate.departure.0 - schedule.start.0) as i64)
197                    * TICKS_PER_SECOND;
198
199                // For each occurrence of the current station in the diagram...
200                for &current_line_index in &previous_indices {
201                    let height = visible_stations[current_line_index].1;
202
203                    // Try to find a WIP edge that was on an adjacent station line.
204                    // Adjacency is defined as being within 1 index in the station list.
205                    // This matching logic relies on the "no A-B-A" constraint to ensure that
206                    // a vehicle line doesn't have multiple valid "previous" segments to choose from.
207                    let matched_idx = local_edges
208                        .iter()
209                        .position(|(_, idx)| current_line_index.abs_diff(*idx) <= 1);
210
211                    let mut segment = if let Some(idx) = matched_idx {
212                        local_edges.swap_remove(idx).0
213                    } else {
214                        Vec::new()
215                    };
216
217                    let arrival_pos = Pos2::new(
218                        ticks_to_screen_x(
219                            arrival_ticks,
220                            &render_context.screen_rect,
221                            render_context.ticks_per_screen_unit,
222                            line_params.tick_offset,
223                        ),
224                        (height - line_params.vertical_offset) * line_params.zoom_y
225                            + render_context.screen_rect.top(),
226                    );
227
228                    let departure_pos = if ce.departure.is_some() {
229                        Some(Pos2::new(
230                            ticks_to_screen_x(
231                                departure_ticks,
232                                &render_context.screen_rect,
233                                render_context.ticks_per_screen_unit,
234                                line_params.tick_offset,
235                            ),
236                            (height - line_params.vertical_offset) * line_params.zoom_y
237                                + render_context.screen_rect.top(),
238                        ))
239                    } else {
240                        None
241                    };
242
243                    segment.push((arrival_pos, departure_pos, *ce_actual));
244
245                    // Check if the next station in the schedule is adjacent in the diagram.
246                    // We only check the immediate neighbors (index -1, 0, +1) of the current station line.
247                    //
248                    // SAFETY: The diagram layout does not contain "A - B - A" arrangements
249                    // where a station B has the same station A as both its predecessor and successor.
250                    // This ensures that there is at most one valid adjacent station line to connect to,
251                    // allowing us to 'break' after the first match without ambiguity.
252                    let mut continued = false;
253                    if let Some((ne_data, _)) = ne {
254                        let (ne_entry, _) = ne_data;
255                        for offset in [-1, 0, 1] {
256                            let next_idx = (current_line_index as isize + offset) as usize;
257                            if let Some((s, _)) = visible_stations.get(next_idx) {
258                                if *s == ne_entry.station {
259                                    next_local_edges.push((segment.clone(), next_idx));
260                                    continued = true;
261                                    break;
262                                }
263                            }
264                        }
265                    }
266
267                    // If the path doesn't continue to an adjacent station, flush this segment.
268                    if !continued {
269                        if segment.len() >= 2 {
270                            segments.push(segment);
271                        }
272                    }
273                }
274
275                // Flush any WIP edges that weren't matched to the current station.
276                for (segment, _) in local_edges.drain(..) {
277                    if segment.len() >= 2 {
278                        segments.push(segment);
279                    }
280                }
281
282                local_edges = next_local_edges;
283                if let Some((ne_data, _)) = ne {
284                    let (ne_entry, _) = ne_data;
285                    previous_indices = visible_stations
286                        .iter()
287                        .enumerate()
288                        .filter_map(|(i, (s, _))| {
289                            if *s == ne_entry.station {
290                                Some(i)
291                            } else {
292                                None
293                            }
294                        })
295                        .collect();
296                }
297            }
298
299            // Final flush of remaining WIP edges for this repetition.
300            for (segment, _) in local_edges {
301                if segment.len() >= 2 {
302                    segments.push(segment);
303                }
304            }
305        }
306
307        rendered_vehicles.push(RenderedVehicle {
308            segments,
309            stroke: line_params.stroke.clone(),
310            entity: vehicle_entity,
311        });
312    }
313
314    buffer.clear();
315    buffer.extend(rendered_vehicles);
316}