1use crate::interface::SelectedElement;
2use crate::interface::tabs::Tab;
3use crate::vehicles::entries::{ActualRouteEntry, VehicleScheduleCache};
4use crate::{
5 interface::widgets::{buttons, timetable_popup},
6 intervals::{Station, StationCache},
7 lines::DisplayedLine,
8 units::time::{Duration, TimetableTime},
9 vehicles::{
10 AdjustTimetableEntry,
11 entries::{TimetableEntry, TimetableEntryCache, TravelMode, VehicleSchedule},
12 },
13};
14use bevy::prelude::*;
15use egui::{
16 Color32, CornerRadius, FontId, Frame, Margin, Painter, Popup, Pos2, Rect, RichText, Sense,
17 Shape, Stroke, Ui, UiBuilder, Vec2, response, vec2,
18};
19use strum::EnumCount;
20use strum_macros::EnumCount;
21mod edit_line;
22
23const TICKS_PER_SECOND: i64 = 100;
25
26#[derive(PartialEq, Eq, Clone, Copy)]
28pub enum SelectedEntityType {
29 Vehicle(Entity),
30 TimetableEntry { entry: Entity, vehicle: Entity },
31 Interval((Entity, Entity)),
32 Station(Entity),
33 Map(Entity),
34}
35
36#[derive(Debug, Clone)]
37pub struct DiagramPageCache {
38 previous_total_drag_delta: Option<f32>,
39 stroke: Stroke,
40 tick_offset: i64,
41 vertical_offset: f32,
42 heights: Option<Vec<(Entity, f32)>>,
43 zoom: Vec2,
44 vehicle_entities: Vec<Entity>,
45}
46
47impl DiagramPageCache {
48 fn get_visible_stations(&self, range: std::ops::Range<f32>) -> &[(Entity, f32)] {
50 let Some(heights) = &self.heights else {
51 return &[];
52 };
53 let first_visible = heights.iter().position(|(_, h)| *h > range.start);
54 let last_visible = heights.iter().rposition(|(_, h)| *h < range.end);
55 if let (Some(mut first_visible), Some(mut last_visible)) = (first_visible, last_visible) {
56 first_visible = first_visible.saturating_sub(2);
58 last_visible = (last_visible + 1).min(heights.len() - 1);
59 &heights[first_visible..=last_visible]
60 } else {
61 &[]
62 }
63 }
64}
65
66impl Default for DiagramPageCache {
67 fn default() -> Self {
68 Self {
69 previous_total_drag_delta: None,
70 tick_offset: 0,
71 vertical_offset: 0.0,
72 stroke: Stroke {
73 width: 1.0,
74 color: Color32::BLACK,
75 },
76 heights: None,
77 zoom: vec2(0.0005, 1.0),
78 vehicle_entities: Vec::new(),
79 }
80 }
81}
82
83type PointData<'a> = (
84 Pos2,
85 Option<Pos2>,
86 (&'a TimetableEntry, &'a TimetableEntryCache),
87 ActualRouteEntry,
88);
89
90struct RenderedVehicle<'a> {
91 segments: Vec<Vec<PointData<'a>>>,
92 stroke: Stroke,
93 entity: Entity,
94}
95
96#[derive(PartialEq, Debug, Default, Clone, EnumCount)]
97enum EditingState {
98 #[default]
99 None,
100 EditingLine,
101}
102
103#[derive(Debug, Clone)]
104pub struct DiagramTab {
105 pub displayed_line_entity: Entity,
106 editing: EditingState,
107 state: DiagramPageCache,
108}
109
110impl DiagramTab {
111 pub fn new(displayed_line_entity: Entity) -> Self {
112 Self {
113 displayed_line_entity,
114 editing: EditingState::default(),
115 state: DiagramPageCache::default(),
116 }
117 }
118}
119
120impl PartialEq for DiagramTab {
121 fn eq(&self, other: &Self) -> bool {
122 self.displayed_line_entity == other.displayed_line_entity
123 }
124}
125
126impl Eq for DiagramTab {}
127
128impl Tab for DiagramTab {
129 const NAME: &'static str = "Diagram";
130 fn main_display(&mut self, world: &mut World, ui: &mut Ui) {
131 if let Err(e) = world.run_system_cached_with(
132 show_diagram,
133 (ui, self.displayed_line_entity, &mut self.state),
134 ) {
135 error!(
136 "UI Error while displaying diagram ({}): {}",
137 self.displayed_line_entity, e
138 )
139 }
140 }
141 fn edit_display(&mut self, world: &mut World, ui: &mut Ui) {
142 let width = ui.available_width();
144 let spacing = ui.spacing().item_spacing.x;
145 let element_width = (width - spacing) / EditingState::COUNT as f32;
146 ui.horizontal(|ui| {
147 if ui
148 .add_sized(
149 [element_width, 30.0],
150 egui::Button::new("None").selected(self.editing == EditingState::None),
151 )
152 .clicked()
153 {
154 self.editing = EditingState::None;
155 }
156 if ui
157 .add_sized(
158 [element_width, 30.0],
159 egui::Button::new("Edit Lines")
160 .selected(self.editing == EditingState::EditingLine),
161 )
162 .clicked()
163 {
164 self.editing = EditingState::EditingLine;
165 }
166 });
167 if self.editing == EditingState::EditingLine {
174 egui::ScrollArea::vertical().show(ui, |ui| {
175 world.run_system_cached_with(edit_line::edit_line, (ui, self.displayed_line_entity))
176 });
177 }
178 }
179 fn display_display(&mut self, world: &mut World, ui: &mut Ui) {
180 let current_tab = world.resource::<SelectedElement>();
181 use super::super::side_panel::*;
182 match current_tab.0 {
183 None => {
184 ui.label(RichText::new("Nothing Selected").italics());
186 }
187 Some(SelectedEntityType::Interval(i)) => {
188 world.run_system_cached_with(interval_stats::show_interval_stats, (ui, i));
189 }
190 Some(SelectedEntityType::Map(i)) => {}
191 Some(SelectedEntityType::Station(s)) => {
192 world.run_system_cached_with(station_stats::show_station_stats, (ui, s));
193 }
194 Some(SelectedEntityType::TimetableEntry { entry, vehicle }) => {}
195 Some(SelectedEntityType::Vehicle(v)) => {
196 world.run_system_cached_with(vehicle_stats::show_vehicle_stats, (ui, v));
197 }
198 }
199 }
200 fn id(&self) -> egui::Id {
201 egui::Id::new(self.displayed_line_entity)
202 }
203 fn scroll_bars(&self) -> [bool; 2] {
204 [false; 2]
205 }
206}
207
208fn show_diagram(
209 (InMut(ui), In(displayed_line_entity), InMut(state)): (
210 InMut<egui::Ui>,
211 In<Entity>,
212 InMut<DiagramPageCache>,
213 ),
214 displayed_lines: Populated<Ref<DisplayedLine>>,
215 vehicles_query: Populated<(Entity, &Name, &VehicleSchedule, &VehicleScheduleCache)>,
216 entry_parents: Query<&ChildOf, With<TimetableEntry>>,
217 timetable_entries: Query<(&TimetableEntry, &TimetableEntryCache)>,
218 station_names: Query<&Name, With<Station>>,
219 station_updated: Query<&StationCache, Changed<StationCache>>,
220 station_caches: Query<&StationCache, With<Station>>,
221 mut selected_element: ResMut<SelectedElement>,
222 mut timetable_adjustment_writer: MessageWriter<AdjustTimetableEntry>,
223 mut visible_stations_scratch: Local<Vec<(Entity, f32)>>,
225) {
226 let Ok(displayed_line) = displayed_lines.get(displayed_line_entity) else {
227 ui.centered_and_justified(|ui| ui.heading("Diagram not found"));
228 return;
229 };
230
231 state.stroke.color = ui.visuals().text_color();
232
233 let entries_updated = displayed_line.is_changed()
234 || state.vehicle_entities.is_empty()
235 || displayed_line
236 .stations
237 .iter()
238 .copied()
239 .any(|(s, _)| station_updated.get(s).is_ok());
240
241 if entries_updated {
242 info!("Updating vehicle entities for diagram display");
243 }
244
245 ui.style_mut().visuals.menu_corner_radius = CornerRadius::ZERO;
246 ui.style_mut().visuals.window_stroke.width = 0.0;
247
248 if entries_updated {
249 for station in displayed_line
250 .stations
251 .iter()
252 .filter_map(|(s, _)| station_caches.get(*s).ok())
253 {
254 station.passing_vehicles(&mut state.vehicle_entities, |e| entry_parents.get(e).ok());
255 }
256 state.vehicle_entities.sort();
257 state.vehicle_entities.dedup();
258 }
259
260 if displayed_line.is_changed() || state.heights.is_none() {
261 ensure_heights(state, &displayed_line);
262 }
263
264 Frame::canvas(ui.style())
265 .fill(if ui.visuals().dark_mode {
266 Color32::BLACK
267 } else {
268 Color32::WHITE
269 })
270 .inner_margin(Margin::ZERO)
271 .show(ui, |ui| {
272 let (response, mut painter) =
273 ui.allocate_painter(ui.available_size_before_wrap(), Sense::click_and_drag());
274
275 let (vertical_visible, horizontal_visible, ticks_per_screen_unit) =
276 calculate_visible_ranges(state, &response.rect);
277
278 visible_stations_scratch.clear();
282 visible_stations_scratch
283 .extend_from_slice(state.get_visible_stations(vertical_visible.clone()));
284 let visible_stations: &[(Entity, f32)] = visible_stations_scratch.as_slice();
285
286 draw_station_lines(
287 state.vertical_offset,
288 state.stroke,
289 &mut painter,
290 &response.rect,
291 state.zoom.y,
292 visible_stations,
293 ui.pixels_per_point(),
294 ui.visuals().text_color(),
295 |e| station_names.get(e).ok().map(|s| s.as_str()),
296 );
297
298 draw_time_lines(
299 state.tick_offset,
300 state.stroke,
301 &mut painter,
302 &response.rect,
303 ticks_per_screen_unit,
304 &horizontal_visible,
305 ui.pixels_per_point(),
306 );
307
308 let rendered_vehicles = collect_rendered_vehicles(
309 |e| timetable_entries.get(e).ok(),
310 |e| vehicles_query.get(e).ok(),
311 visible_stations,
312 &horizontal_visible,
313 ticks_per_screen_unit,
314 state,
315 &response.rect,
316 &state.vehicle_entities,
317 );
318
319 if response.clicked()
320 && let Some(pos) = response.interact_pointer_pos()
321 {
322 handle_input_selection(
323 pos,
324 &rendered_vehicles,
325 visible_stations,
326 &response.rect,
327 state.vertical_offset,
328 state.zoom.y,
329 &mut selected_element,
330 );
331 }
332
333 let background_strength = ui.ctx().animate_bool(
334 ui.id().with("background animation"),
335 selected_element.is_some(),
336 );
337
338 draw_vehicles(&mut painter, &rendered_vehicles, &mut selected_element);
339
340 if background_strength > 0.1 {
341 painter.rect_filled(painter.clip_rect(), CornerRadius::ZERO, {
342 let amt = (background_strength * 180.0) as u8;
343 if ui.ctx().theme().default_visuals().dark_mode {
344 Color32::from_black_alpha(amt)
345 } else {
346 Color32::from_white_alpha(amt)
347 }
348 });
349 }
350
351 let show_button = state.zoom.x.min(state.zoom.y) > 0.0002;
352 let button_strength = ui
353 .ctx()
354 .animate_bool(ui.id().with("all buttons animation"), show_button);
355
356 match selected_element.0 {
357 None => {}
358 Some(SelectedEntityType::Vehicle(v)) => {
359 draw_vehicle_selection_overlay(
360 ui,
361 &mut painter,
362 &rendered_vehicles,
363 state,
364 background_strength,
365 button_strength,
366 v,
367 &mut timetable_adjustment_writer,
368 &station_names,
369 );
370 }
371 Some(SelectedEntityType::TimetableEntry {
372 entry: e,
373 vehicle: v,
374 }) => {}
375 Some(SelectedEntityType::Interval(i)) => {
376 draw_interval_selection_overlay(
377 ui,
378 background_strength,
379 &mut painter,
380 response.rect,
381 state.vertical_offset,
382 state.zoom.y,
383 i,
384 visible_stations,
385 );
386 }
387 Some(SelectedEntityType::Station(s)) => {
388 draw_station_selection_overlay(
389 ui,
390 background_strength,
391 &mut painter,
392 response.rect,
393 state.vertical_offset,
394 state.zoom.y,
395 s,
396 visible_stations,
397 );
398 }
399 Some(SelectedEntityType::Map(_)) => {
400 todo!("Do this bro")
401 }
402 }
403
404 handle_navigation(ui, &response, state);
405 });
406}
407
408fn ensure_heights(state: &mut DiagramPageCache, displayed_line: &DisplayedLine) {
409 let mut current_height = 0.0;
410 let mut heights = Vec::new();
411 for (station, distance) in &displayed_line.stations {
412 current_height += distance.abs().log2().max(1.0) * 15f32;
413 heights.push((*station, current_height))
414 }
415 state.heights = Some(heights);
416}
417
418fn calculate_visible_ranges(
419 state: &DiagramPageCache,
420 rect: &Rect,
421) -> (std::ops::Range<f32>, std::ops::Range<i64>, f64) {
422 let vertical_visible =
423 state.vertical_offset..rect.height() / state.zoom.y + state.vertical_offset;
424 let horizontal_visible =
425 state.tick_offset..state.tick_offset + (rect.width() as f64 / state.zoom.x as f64) as i64;
426 let ticks_per_screen_unit =
427 (horizontal_visible.end - horizontal_visible.start) as f64 / rect.width() as f64;
428 (vertical_visible, horizontal_visible, ticks_per_screen_unit)
429}
430
431fn collect_rendered_vehicles<'a, F, G>(
435 get_timetable_entries: F,
436 get_vehicles: G,
437 visible_stations: &[(Entity, f32)],
438 horizontal_visible: &std::ops::Range<i64>,
439 ticks_per_screen_unit: f64,
440 state: &DiagramPageCache,
441 screen_rect: &Rect,
442 vehicles: &[Entity],
443) -> Vec<RenderedVehicle<'a>>
444where
445 F: Fn(Entity) -> Option<(&'a TimetableEntry, &'a TimetableEntryCache)> + Copy + 'a,
446 G: Fn(
447 Entity,
448 ) -> Option<(
449 Entity,
450 &'a Name,
451 &'a VehicleSchedule,
452 &'a VehicleScheduleCache,
453 )> + 'a,
454{
455 let mut rendered_vehicles = Vec::new();
456
457 for (vehicle_entity, _name, schedule, schedule_cache) in
458 vehicles.iter().copied().filter_map(|e| get_vehicles(e))
459 {
460 let Some(visible_sets) = schedule_cache.get_entries_range(
462 schedule,
463 TimetableTime((horizontal_visible.start / TICKS_PER_SECOND) as i32)
464 ..TimetableTime((horizontal_visible.end / TICKS_PER_SECOND) as i32),
465 get_timetable_entries,
466 ) else {
467 continue;
468 };
469
470 let mut segments = Vec::new();
471
472 for (initial_offset, set) in visible_sets {
473 let mut local_edges: Vec<(Vec<PointData<'a>>, usize)> = Vec::new();
475 let mut previous_indices: Vec<usize> = Vec::new();
476
477 if let Some((ce_data, _)) = set.first() {
479 let (ce, _) = ce_data;
480 previous_indices = visible_stations
481 .iter()
482 .enumerate()
483 .filter_map(|(i, (s, _))| if *s == ce.station { Some(i) } else { None })
484 .collect();
485 }
486
487 for entry_idx in 0..set.len() {
488 let (ce_data, ce_actual) = &set[entry_idx];
489 let (ce, ce_cache) = ce_data;
490 let ne = set.get(entry_idx + 1);
491
492 if previous_indices.is_empty() {
494 if let Some((ne_data, _)) = ne {
495 let (ne_entry, _) = ne_data;
496 previous_indices = visible_stations
497 .iter()
498 .enumerate()
499 .filter_map(|(i, (s, _))| {
500 if *s == ne_entry.station {
501 Some(i)
502 } else {
503 None
504 }
505 })
506 .collect();
507 }
508 for (segment, _) in local_edges.drain(..) {
509 if segment.len() >= 2 {
510 segments.push(segment);
511 }
512 }
513 continue;
514 }
515
516 let mut next_local_edges = Vec::new();
517
518 let Some(estimate) = ce_cache.estimate.as_ref() else {
520 for (segment, _) in local_edges.drain(..) {
521 if segment.len() >= 2 {
522 segments.push(segment);
523 }
524 }
525 if let Some((ne_data, _)) = ne {
526 let (ne_entry, _) = ne_data;
527 previous_indices = visible_stations
528 .iter()
529 .enumerate()
530 .filter_map(|(i, (s, _))| {
531 if *s == ne_entry.station {
532 Some(i)
533 } else {
534 None
535 }
536 })
537 .collect();
538 }
539 continue;
540 };
541
542 let arrival_ticks = (initial_offset.0 as i64
544 + (estimate.arrival.0 - schedule.start.0) as i64)
545 * TICKS_PER_SECOND;
546 let departure_ticks = (initial_offset.0 as i64
547 + (estimate.departure.0 - schedule.start.0) as i64)
548 * TICKS_PER_SECOND;
549
550 for ¤t_line_index in &previous_indices {
552 let height = visible_stations[current_line_index].1;
553
554 let matched_idx = local_edges
559 .iter()
560 .position(|(_, idx)| current_line_index.abs_diff(*idx) <= 1);
561
562 let mut segment = if let Some(idx) = matched_idx {
563 local_edges.swap_remove(idx).0
564 } else {
565 Vec::new()
566 };
567
568 let arrival_pos = Pos2::new(
569 ticks_to_screen_x(
570 arrival_ticks,
571 screen_rect,
572 ticks_per_screen_unit,
573 state.tick_offset,
574 ),
575 (height - state.vertical_offset) * state.zoom.y + screen_rect.top(),
576 );
577
578 let departure_pos = if ce.departure.is_some() {
579 Some(Pos2::new(
580 ticks_to_screen_x(
581 departure_ticks,
582 screen_rect,
583 ticks_per_screen_unit,
584 state.tick_offset,
585 ),
586 (height - state.vertical_offset) * state.zoom.y + screen_rect.top(),
587 ))
588 } else {
589 None
590 };
591
592 segment.push((arrival_pos, departure_pos, (*ce, *ce_cache), *ce_actual));
593
594 let mut continued = false;
602 if let Some((ne_data, _)) = ne {
603 let (ne_entry, _) = ne_data;
604 for offset in [-1, 0, 1] {
605 let next_idx = (current_line_index as isize + offset) as usize;
606 if let Some((s, _)) = visible_stations.get(next_idx) {
607 if *s == ne_entry.station {
608 next_local_edges.push((segment.clone(), next_idx));
609 continued = true;
610 break;
611 }
612 }
613 }
614 }
615
616 if !continued {
618 if segment.len() >= 2 {
619 segments.push(segment);
620 }
621 }
622 }
623
624 for (segment, _) in local_edges.drain(..) {
626 if segment.len() >= 2 {
627 segments.push(segment);
628 }
629 }
630
631 local_edges = next_local_edges;
632 if let Some((ne_data, _)) = ne {
633 let (ne_entry, _) = ne_data;
634 previous_indices = visible_stations
635 .iter()
636 .enumerate()
637 .filter_map(|(i, (s, _))| {
638 if *s == ne_entry.station {
639 Some(i)
640 } else {
641 None
642 }
643 })
644 .collect();
645 }
646 }
647
648 for (segment, _) in local_edges {
650 if segment.len() >= 2 {
651 segments.push(segment);
652 }
653 }
654 }
655
656 rendered_vehicles.push(RenderedVehicle {
657 segments,
658 stroke: state.stroke,
659 entity: vehicle_entity,
660 });
661 }
662 rendered_vehicles
663}
664
665fn handle_input_selection(
666 pointer_pos: Pos2,
667 rendered_vehicles: &[RenderedVehicle],
668 visible_stations: &[(Entity, f32)],
669 screen_rect: &Rect,
670 vertical_offset: f32,
671 zoom_y: f32,
672 selected_entity: &mut Option<SelectedEntityType>,
673) {
674 const VEHICLE_SELECTION_RADIUS: f32 = 7.0;
675 const STATION_SELECTION_RADIUS: f32 = VEHICLE_SELECTION_RADIUS;
676 if selected_entity.is_some() {
677 *selected_entity = None;
678 return;
679 };
680 let mut found: Option<SelectedEntityType> = None;
681 'check_selected: for vehicle in rendered_vehicles {
682 for segment in &vehicle.segments {
683 let mut points = segment
684 .iter()
685 .flat_map(|(a_pos, d_pos, ..)| std::iter::once(*a_pos).chain(*d_pos));
686
687 if let Some(mut curr) = points.next() {
688 for next in points {
689 let a = pointer_pos.x - curr.x;
690 let b = pointer_pos.y - curr.y;
691 let c = next.x - curr.x;
692 let d = next.y - curr.y;
693 let dot = a * c + b * d;
694 let len_sq = c * c + d * d;
695 if len_sq == 0.0 {
696 continue;
697 }
698 let t = (dot / len_sq).clamp(0.0, 1.0);
699 let px = curr.x + t * c;
700 let py = curr.y + t * d;
701 let dx = pointer_pos.x - px;
702 let dy = pointer_pos.y - py;
703
704 if dx * dx + dy * dy < VEHICLE_SELECTION_RADIUS.powi(2) {
705 found = Some(SelectedEntityType::Vehicle(vehicle.entity));
706 break 'check_selected;
707 }
708 curr = next;
709 }
710 }
711 }
712 }
713 if found.is_some() {
714 *selected_entity = found;
715 return;
716 }
717 for (station_entity, height) in visible_stations {
719 let y = (*height - vertical_offset) * zoom_y + screen_rect.top();
720 if (y - STATION_SELECTION_RADIUS..y + STATION_SELECTION_RADIUS).contains(&pointer_pos.y) {
721 found = Some(SelectedEntityType::Station(*station_entity));
722 break;
723 }
724 }
725 if found.is_some() {
726 *selected_entity = found;
727 return;
728 }
729 for w in visible_stations.windows(2) {
730 let [(e1, h1), (e2, h2)] = w else {
731 continue;
732 };
733 let y1 = (*h1 - vertical_offset) * zoom_y + screen_rect.top();
734 let y2 = (*h2 - vertical_offset) * zoom_y + screen_rect.top();
735 let (min_y, max_y) = if y1 <= y2 { (y1, y2) } else { (y2, y1) };
736 if (min_y..max_y).contains(&pointer_pos.y) {
737 found = Some(SelectedEntityType::Interval((*e1, *e2)));
738 break;
739 }
740 }
741 if found.is_some() {
742 *selected_entity = found;
743 return;
744 }
745}
746
747fn draw_vehicles(
748 painter: &mut Painter,
749 rendered_vehicles: &[RenderedVehicle],
750 selected_entity: &Option<SelectedEntityType>,
751) {
752 let mut selected_vehicle = None;
753 for vehicle in rendered_vehicles {
754 if selected_vehicle.is_none()
755 && let Some(selected_entity) = selected_entity
756 && matches!(selected_entity, SelectedEntityType::Vehicle(e) if *e == vehicle.entity)
757 {
758 selected_vehicle = Some(vehicle);
759 continue;
760 }
761
762 for segment in &vehicle.segments {
763 let points = segment
764 .iter()
765 .flat_map(|(a, d, ..)| std::iter::once(*a).chain(*d))
766 .collect::<Vec<_>>();
767 painter.line(points, vehicle.stroke);
768 }
769 }
770}
771
772fn draw_vehicle_selection_overlay(
773 ui: &mut Ui,
774 painter: &mut Painter,
775 rendered_vehicles: &[RenderedVehicle],
776 state: &mut DiagramPageCache,
777 line_strength: f32,
778 button_strength: f32,
779 selected_entity: Entity,
780 timetable_adjustment_writer: &mut MessageWriter<AdjustTimetableEntry>,
781 station_names: &Query<&Name, With<Station>>,
782) {
783 let Some(vehicle) = rendered_vehicles
784 .iter()
785 .find(|v| selected_entity == v.entity)
786 else {
787 return;
788 };
789
790 let mut stroke = vehicle.stroke;
791 stroke.width = line_strength * 3.0 * stroke.width + stroke.width;
792
793 for (line_index, segment) in vehicle.segments.iter().enumerate() {
794 let mut line_vec = Vec::with_capacity(segment.len() * 2);
795
796 for idx in 0..segment.len().saturating_sub(1) {
797 let (arrival_pos, departure_pos, (_, entry_cache), _) = segment[idx];
798 let (next_arrival_pos, _, (next_entry, next_entry_cache), _) = segment[idx + 1];
799 let signal_stroke = Stroke {
800 width: 1.0 + line_strength,
801 color: if matches!(next_entry.arrival, TravelMode::For(_)) {
802 Color32::BLUE
803 } else {
804 Color32::ORANGE
805 },
806 };
807
808 line_vec.push(arrival_pos);
809 let mut curr_pos = if let Some(d_pos) = departure_pos {
810 line_vec.push(d_pos);
811 d_pos
812 } else {
813 arrival_pos
814 };
815
816 let mut next_pos = next_arrival_pos;
817
818 curr_pos.y += 5.0;
819 next_pos.y += 5.0;
820
821 signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut curr_pos.x);
822 signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut curr_pos.y);
823 signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut next_pos.x);
824 signal_stroke.round_center_to_pixel(ui.pixels_per_point(), &mut next_pos.y);
825
826 let duration = next_entry_cache.estimate.as_ref().unwrap().arrival
827 - entry_cache.estimate.as_ref().unwrap().departure;
828
829 let points = if next_pos.y <= curr_pos.y {
830 vec![curr_pos, Pos2::new(next_pos.x, curr_pos.y), next_pos]
831 } else {
832 vec![curr_pos, Pos2::new(curr_pos.x, next_pos.y), next_pos]
833 };
834 painter.add(Shape::line(points, signal_stroke));
835
836 if duration != Duration(0) {
837 let time = duration.to_hms();
838 let text = format!("{}:{:02}", time.0 * 60 + time.1, time.2);
839 let duration_text = painter.layout_no_wrap(
840 text,
841 egui::FontId::monospace(15.0),
842 signal_stroke.color,
843 );
844 painter.galley(
845 Pos2 {
846 x: (curr_pos.x + next_pos.x - duration_text.size().x) / 2.0,
847 y: curr_pos.y.max(next_pos.y) + 1.0,
848 },
849 duration_text,
850 signal_stroke.color,
851 );
852 }
853 }
854
855 if let Some(last_pos) = segment.last() {
856 line_vec.push(last_pos.0);
857 if let Some(departure) = last_pos.1 {
858 line_vec.push(departure)
859 }
860 }
861 painter.line(line_vec, stroke);
862
863 let mut previous_entry: Option<(
864 Pos2,
865 Option<Pos2>,
866 (&TimetableEntry, &TimetableEntryCache),
867 ActualRouteEntry,
868 )> = None;
869 if button_strength <= 0.0 {
870 continue;
871 }
872 for fragment in segment
873 .iter()
874 .cloned()
875 .filter(|(_, _, _, a)| matches!(a, ActualRouteEntry::Nominal(_)))
876 {
877 let (mut arrival_pos, maybe_departure_pos, (entry, entry_cache), entry_entity) =
878 fragment;
879 const HANDLE_SIZE: f32 = 12.0;
880 const CIRCLE_HANDLE_SIZE: f32 = 7.0;
881 const TRIANGLE_HANDLE_SIZE: f32 = 10.0;
882 const DASH_HANDLE_SIZE: f32 = 9.0;
883 let departure_pos: Pos2;
884 if let Some(unwrapped_pos) = maybe_departure_pos {
885 if (arrival_pos.x - unwrapped_pos.x).abs() < HANDLE_SIZE {
886 let midpoint_x = (arrival_pos.x + unwrapped_pos.x) / 2.0;
887 arrival_pos.x = midpoint_x - HANDLE_SIZE / 2.0;
888 let mut pos = unwrapped_pos;
889 pos.x = midpoint_x + HANDLE_SIZE / 2.0;
890 departure_pos = pos;
891 } else {
892 departure_pos = unwrapped_pos;
893 }
894 } else {
895 arrival_pos.x -= HANDLE_SIZE / 2.0;
896 let mut pos = arrival_pos;
897 pos.x += HANDLE_SIZE;
898 departure_pos = pos;
899 };
900 let arrival_point_response =
901 ui.place(Rect::from_pos(arrival_pos).expand(5.2), |ui: &mut Ui| {
902 let (rect, resp) =
903 ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());
904 ui
905 .scope_builder(
906 UiBuilder::new()
907 .sense(resp.sense)
908 .max_rect(rect)
909 .id(entry_entity.inner().to_bits() as u128
910 | (line_index as u128) << 64),
911 |ui| {
912 ui.set_min_size(ui.available_size());
913 let response = ui.response();
914 let fill = if response.hovered() {
915 Color32::GRAY
916 } else {
917 Color32::WHITE
918 }
919 .linear_multiply(button_strength);
920 let handle_stroke = Stroke {
921 width: 2.5,
922 color: stroke.color.linear_multiply(button_strength),
923 };
924 match entry.arrival {
925 TravelMode::At(_) => buttons::circle_button_shape(
926 painter,
927 arrival_pos,
928 CIRCLE_HANDLE_SIZE,
929 handle_stroke,
930 fill,
931 ),
932 TravelMode::For(_) => buttons::dash_button_shape(
933 painter,
934 arrival_pos,
935 DASH_HANDLE_SIZE,
936 handle_stroke,
937 fill,
938 ),
939 TravelMode::Flexible => buttons::triangle_button_shape(
940 painter,
941 arrival_pos,
942 TRIANGLE_HANDLE_SIZE,
943 handle_stroke,
944 fill,
945 ),
946 };
947 },
948 )
949 .response
950 });
951
952 if arrival_point_response.drag_started() {
953 state.previous_total_drag_delta = None;
954 }
955 if let Some(total_drag_delta) = arrival_point_response.total_drag_delta() {
956 let previous_drag_delta = state.previous_total_drag_delta.unwrap_or(0.0);
957 let duration = Duration(
958 ((total_drag_delta.x as f64 - previous_drag_delta as f64)
959 / state.zoom.x as f64
960 / TICKS_PER_SECOND as f64) as i32,
961 );
962 if duration != Duration(0) {
963 timetable_adjustment_writer.write(AdjustTimetableEntry {
964 entity: entry_entity.inner(),
965 adjustment: crate::vehicles::TimetableAdjustment::AdjustArrivalTime(
966 duration,
967 ),
968 });
969 state.previous_total_drag_delta = Some(
970 previous_drag_delta
971 + (duration.0 as f64 * TICKS_PER_SECOND as f64 * state.zoom.x as f64)
972 as f32,
973 );
974 }
975 }
976 if arrival_point_response.drag_stopped() {
977 state.previous_total_drag_delta = None;
978 }
979 if arrival_point_response.dragged() {
980 arrival_point_response.show_tooltip_ui(|ui| {
981 ui.label(entry_cache.estimate.as_ref().unwrap().arrival.to_string());
982 ui.label(
983 station_names
984 .get(entry.station)
985 .map_or("??", |s| s.as_str()),
986 );
987 });
988 } else {
989 Popup::menu(&arrival_point_response)
990 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
991 .show(|ui| {
992 timetable_popup::popup(
993 entry_entity.inner(),
994 (entry, entry_cache),
995 previous_entry.map(|e| {
996 let e = e.2;
997 (e.0, e.1)
998 }),
999 timetable_adjustment_writer,
1000 ui,
1001 true,
1002 );
1003 });
1004 }
1005
1006 let departure_point_response =
1007 ui.put(Rect::from_pos(departure_pos).expand(4.5), |ui: &mut Ui| {
1008 let (rect, resp) = ui.allocate_exact_size(
1009 ui.available_size(),
1010 if matches!(
1011 entry.departure.unwrap_or(TravelMode::Flexible),
1012 TravelMode::Flexible
1013 ) {
1014 Sense::click()
1015 } else {
1016 Sense::click_and_drag()
1017 },
1018 );
1019 ui.scope_builder(
1020 UiBuilder::new()
1021 .sense(resp.sense)
1022 .max_rect(rect)
1023 .id((entry_entity.inner().to_bits() as u128
1024 | (line_index as u128) << 64)
1025 ^ (1 << 127)),
1026 |ui| {
1027 ui.set_min_size(ui.available_size());
1028 let response = ui.response();
1029 let fill = if response.hovered() {
1030 Color32::GRAY
1031 } else {
1032 Color32::WHITE
1033 }
1034 .linear_multiply(button_strength);
1035 let handle_stroke = Stroke {
1036 width: 2.5,
1037 color: stroke.color.linear_multiply(button_strength),
1038 };
1039 match entry.departure {
1040 Some(TravelMode::At(_)) => buttons::circle_button_shape(
1041 painter,
1042 departure_pos,
1043 CIRCLE_HANDLE_SIZE,
1044 handle_stroke,
1045 fill,
1046 ),
1047 Some(TravelMode::For(_)) => buttons::dash_button_shape(
1048 painter,
1049 departure_pos,
1050 DASH_HANDLE_SIZE,
1051 handle_stroke,
1052 fill,
1053 ),
1054 Some(TravelMode::Flexible) => buttons::triangle_button_shape(
1055 painter,
1056 departure_pos,
1057 TRIANGLE_HANDLE_SIZE,
1058 handle_stroke,
1059 fill,
1060 ),
1061 None => buttons::double_triangle(
1062 painter,
1063 departure_pos,
1064 DASH_HANDLE_SIZE,
1065 handle_stroke,
1066 fill,
1067 ),
1068 };
1069 },
1070 )
1071 .response
1072 });
1073
1074 if departure_point_response.drag_started() {
1075 state.previous_total_drag_delta = None;
1076 }
1077 if let Some(total_drag_delta) = departure_point_response.total_drag_delta() {
1078 let previous_drag_delta = state.previous_total_drag_delta.unwrap_or(0.0);
1079 let duration = Duration(
1080 ((total_drag_delta.x as f64 - previous_drag_delta as f64)
1081 / state.zoom.x as f64
1082 / TICKS_PER_SECOND as f64) as i32,
1083 );
1084 if duration != Duration(0) {
1085 timetable_adjustment_writer.write(AdjustTimetableEntry {
1086 entity: entry_entity.inner(),
1087 adjustment: crate::vehicles::TimetableAdjustment::AdjustDepartureTime(
1088 duration,
1089 ),
1090 });
1091 state.previous_total_drag_delta = Some(
1092 previous_drag_delta
1093 + (duration.0 as f64 * TICKS_PER_SECOND as f64 * state.zoom.x as f64)
1094 as f32,
1095 );
1096 }
1097 }
1098 if departure_point_response.drag_stopped() {
1099 state.previous_total_drag_delta = None;
1100 }
1101 if departure_point_response.dragged() {
1102 departure_point_response.show_tooltip_ui(|ui| {
1103 ui.label(entry_cache.estimate.as_ref().unwrap().departure.to_string());
1104 ui.label(
1105 station_names
1106 .get(entry.station)
1107 .map_or("??", |s| s.as_str()),
1108 );
1109 });
1110 } else {
1111 Popup::menu(&departure_point_response)
1112 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
1113 .show(|ui| {
1114 timetable_popup::popup(
1115 entry_entity.inner(),
1116 (entry, entry_cache),
1117 previous_entry.map(|e| {
1118 let e = e.2;
1119 (e.0, e.1)
1120 }),
1121 timetable_adjustment_writer,
1122 ui,
1123 false,
1124 );
1125 });
1126 previous_entry = Some(fragment);
1127 }
1128 }
1129 }
1130}
1131
1132fn draw_station_selection_overlay(
1133 ui: &mut Ui,
1134 strength: f32,
1135 painter: &mut Painter,
1136 screen_rect: Rect,
1137 vertical_offset: f32,
1138 zoom_y: f32,
1139 station_entity: Entity,
1140 visible_stations: &[(Entity, f32)],
1141) {
1142 let stations = visible_stations
1143 .iter()
1144 .copied()
1145 .filter_map(|(s, h)| if s == station_entity { Some(h) } else { None });
1146 for station in stations {
1147 let station_height = (station - vertical_offset) * zoom_y + screen_rect.top();
1148 painter.rect(
1149 Rect::from_two_pos(
1150 Pos2 {
1151 x: screen_rect.left(),
1152 y: station_height,
1153 },
1154 Pos2 {
1155 x: screen_rect.right(),
1156 y: station_height,
1157 },
1158 )
1159 .expand2(Vec2 { x: -1.0, y: 7.0 }),
1160 4,
1161 Color32::BLUE.linear_multiply(strength * 0.5),
1162 Stroke::new(1.0, Color32::BLUE.linear_multiply(strength)),
1163 egui::StrokeKind::Middle,
1164 );
1165 }
1166}
1167
1168fn draw_interval_selection_overlay(
1169 ui: &mut Ui,
1170 strength: f32,
1171 painter: &mut Painter,
1172 screen_rect: Rect,
1173 vertical_offset: f32,
1174 zoom_y: f32,
1175 (s1, s2): (Entity, Entity),
1176 visible_stations: &[(Entity, f32)],
1177) {
1178 for w in visible_stations.windows(2) {
1179 let [(e1, h1), (e2, h2)] = w else { continue };
1180 if !((*e1 == s1 && *e2 == s2) || (*e1 == s2 && *e2 == s1)) {
1181 continue;
1182 }
1183 let station_height_1 = (h1 - vertical_offset) * zoom_y + screen_rect.top();
1184 let station_height_2 = (h2 - vertical_offset) * zoom_y + screen_rect.top();
1185 painter.rect(
1186 Rect::from_two_pos(
1187 Pos2 {
1188 x: screen_rect.left(),
1189 y: station_height_1,
1190 },
1191 Pos2 {
1192 x: screen_rect.right(),
1193 y: station_height_2,
1194 },
1195 )
1196 .expand2(Vec2 { x: -1.0, y: 7.0 }),
1197 4,
1198 Color32::GREEN.linear_multiply(strength * 0.5),
1199 Stroke::new(1.0, Color32::GREEN.linear_multiply(strength)),
1200 egui::StrokeKind::Middle,
1201 );
1202 }
1203}
1204
1205fn handle_navigation(ui: &mut Ui, response: &response::Response, state: &mut DiagramPageCache) {
1206 let mut zoom_delta: Vec2 = Vec2::default();
1207 let mut translation_delta: Vec2 = Vec2::default();
1208 ui.input(|input| {
1209 zoom_delta = input.zoom_delta_2d();
1210 translation_delta = input.translation_delta();
1211 });
1212 if let Some(pos) = response.hover_pos() {
1213 let old_zoom = state.zoom;
1214 let mut new_zoom = state.zoom * zoom_delta;
1215 new_zoom.x = new_zoom.x.clamp(0.00001, 0.4);
1216 new_zoom.y = new_zoom.y.clamp(0.025, 2048.0);
1217 let rel_pos = (pos - response.rect.min) / response.rect.size();
1218 let world_width_before = response.rect.width() as f64 / old_zoom.x as f64;
1219 let world_width_after = response.rect.width() as f64 / new_zoom.x as f64;
1220 let world_pos_before_x = state.tick_offset as f64 + rel_pos.x as f64 * world_width_before;
1221 let new_tick_offset =
1222 (world_pos_before_x - rel_pos.x as f64 * world_width_after).round() as i64;
1223 let world_height_before = response.rect.height() as f64 / old_zoom.y as f64;
1224 let world_height_after = response.rect.height() as f64 / new_zoom.y as f64;
1225 let world_pos_before_y =
1226 state.vertical_offset as f64 + rel_pos.y as f64 * world_height_before;
1227 let new_vertical_offset =
1228 (world_pos_before_y - rel_pos.y as f64 * world_height_after) as f32;
1229 state.zoom = new_zoom;
1230 state.tick_offset = new_tick_offset;
1231 state.vertical_offset = new_vertical_offset;
1232 }
1233
1234 let ticks_per_screen_unit = 1.0 / state.zoom.x as f64;
1235
1236 state.tick_offset -=
1237 (ticks_per_screen_unit * (response.drag_delta().x + translation_delta.x) as f64) as i64;
1238 state.vertical_offset -= (response.drag_delta().y + translation_delta.y) / state.zoom.y;
1239 state.tick_offset = state.tick_offset.clamp(
1240 -366 * 86400 * TICKS_PER_SECOND,
1241 366 * 86400 * TICKS_PER_SECOND
1242 - (response.rect.width() as f64 / state.zoom.x as f64) as i64,
1243 );
1244 const TOP_BOTTOM_PADDING: f32 = 30.0;
1245 let max_height = state
1246 .heights
1247 .as_ref()
1248 .unwrap()
1249 .last()
1250 .map(|(_, h)| *h)
1251 .unwrap_or(0.0);
1252 state.vertical_offset = if response.rect.height() / state.zoom.y
1253 > (max_height + TOP_BOTTOM_PADDING * 2.0 / state.zoom.y)
1254 {
1255 (-response.rect.height() / state.zoom.y + max_height) / 2.0
1256 } else {
1257 state.vertical_offset.clamp(
1258 -TOP_BOTTOM_PADDING / state.zoom.y,
1259 max_height - response.rect.height() / state.zoom.y + TOP_BOTTOM_PADDING / state.zoom.y,
1260 )
1261 }
1262}
1263
1264fn draw_station_lines<'a, F>(
1265 vertical_offset: f32,
1266 stroke: Stroke,
1267 painter: &mut Painter,
1268 screen_rect: &Rect,
1269 zoom: f32,
1270 to_draw: &[(Entity, f32)],
1271 pixels_per_point: f32,
1272 text_color: Color32,
1273 mut get_station_name: F,
1274) where
1275 F: FnMut(Entity) -> Option<&'a str>,
1276{
1277 for (entity, height) in to_draw.iter().copied() {
1279 let mut draw_height = (height - vertical_offset) * zoom + screen_rect.top();
1280 stroke.round_center_to_pixel(pixels_per_point, &mut draw_height);
1281 painter.hline(
1282 screen_rect.left()..=screen_rect.right(),
1283 draw_height,
1284 stroke,
1285 );
1286 let Some(station_name) = get_station_name(entity) else {
1287 continue;
1288 };
1289 let layout = painter.layout_no_wrap(
1290 station_name.to_string(),
1291 egui::FontId::proportional(13.0),
1292 text_color,
1293 );
1294 let layout_pos = Pos2 {
1295 x: screen_rect.left(),
1296 y: draw_height - layout.size().y,
1297 };
1298 painter.galley(layout_pos, layout, text_color);
1299 }
1300}
1301
1302fn ticks_to_screen_x(
1303 ticks: i64,
1304 screen_rect: &Rect,
1305 ticks_per_screen_unit: f64,
1306 offset_ticks: i64,
1307) -> f32 {
1308 let base = (ticks - offset_ticks) as f64 / ticks_per_screen_unit;
1309 screen_rect.left() + base as f32
1310}
1311
1312fn draw_time_lines(
1314 tick_offset: i64,
1315 stroke: Stroke,
1316 painter: &mut Painter,
1317 screen_rect: &Rect,
1318 ticks_per_screen_unit: f64,
1319 visible_ticks: &std::ops::Range<i64>,
1320 pixels_per_point: f32,
1321) {
1322 const MAX_SCREEN_WIDTH: f64 = 64.0;
1323 const MIN_SCREEN_WIDTH: f64 = 32.0;
1324 const SIZES: &[i64] = &[
1325 TICKS_PER_SECOND * 1, TICKS_PER_SECOND * 10, TICKS_PER_SECOND * 30, TICKS_PER_SECOND * 60, TICKS_PER_SECOND * 60 * 5, TICKS_PER_SECOND * 60 * 10, TICKS_PER_SECOND * 60 * 30, TICKS_PER_SECOND * 60 * 60, TICKS_PER_SECOND * 60 * 60 * 4, TICKS_PER_SECOND * 60 * 60 * 24, ];
1336 let mut drawn: Vec<i64> = Vec::with_capacity(30);
1337
1338 let first_visible_position = SIZES
1340 .iter()
1341 .position(|s| *s as f64 / ticks_per_screen_unit * 1.5 > MIN_SCREEN_WIDTH)
1342 .unwrap_or(0);
1343 let visible = &SIZES[first_visible_position..];
1344 for (i, spacing) in visible.iter().enumerate().rev() {
1345 let first = visible_ticks.start - visible_ticks.start.rem_euclid(*spacing) - spacing;
1346 let mut tick = first;
1347 let strength = (((*spacing as f64 / ticks_per_screen_unit * 1.5) - MIN_SCREEN_WIDTH)
1348 / (MAX_SCREEN_WIDTH - MIN_SCREEN_WIDTH))
1349 .clamp(0.0, 1.0);
1350 if strength < 0.1 {
1351 continue;
1352 }
1353 let mut current_stroke = stroke;
1354 if strength.is_finite() {
1355 current_stroke.color = current_stroke.color.gamma_multiply(strength as f32);
1357 }
1358 current_stroke.width = 0.5;
1359 while tick <= visible_ticks.end {
1360 tick += *spacing;
1361 if drawn.contains(&tick) {
1362 continue;
1363 }
1364 let mut x = ticks_to_screen_x(tick, screen_rect, ticks_per_screen_unit, tick_offset);
1365 current_stroke.round_center_to_pixel(pixels_per_point, &mut x);
1366 painter.vline(x, screen_rect.top()..=screen_rect.bottom(), current_stroke);
1367 drawn.push(tick);
1368 let time = TimetableTime((tick / 100) as i32);
1369 let text = match i + first_visible_position {
1370 0..=2 => time.to_hmsd().2.to_string(),
1371 3..=8 => format!("{}:{:02}", time.to_hmsd().0, time.to_hmsd().1),
1372 _ => time.to_string(),
1373 };
1374 let label = painter.layout_no_wrap(text, FontId::monospace(13.0), current_stroke.color);
1375 painter.galley(
1376 Pos2 {
1377 x: x - label.size().x / 2.0,
1378 y: screen_rect.top(),
1379 },
1380 label,
1381 current_stroke.color,
1382 );
1383 }
1384 }
1385}