1use crate::map::animation::Animation;
105use crate::map::objects::{
106 CometDirection, ContextMenuManager, MapBounds, MapLabel, MapPoint, MapSegment, MapSettings,
107 MarkerContext, NodeAnimation, NotificationContext, RawLine, RawPoint, SegmentAnimation,
108 SteadyAnimation, SteadySegmentAnimation, TextSettings, VisibilitySetting,
109};
110use crate::map::theme::{ColorMode, MapTheme, Style, Theme};
111use egui::{widgets::*, *};
112use kdtree::KdTree;
113use kdtree::distance::squared_euclidean;
114use std::collections::{HashMap, HashSet};
115use std::rc::Rc;
116use std::time::Instant;
117
118use self::objects::{NodeTemplate, SegmentTemplate};
119
120pub mod animation;
121pub mod objects;
122pub mod theme;
123
124const SEGMENT_EFFECT_ALPHA_BOOST: f32 = 0.2;
131
132fn scale_alpha(color: Color32, factor: f32) -> Color32 {
144 let [r, g, b, a] = color.to_srgba_unmultiplied();
145 Color32::from_rgba_unmultiplied(r, g, b, (a as f32 * factor.clamp(0.0, 1.0)).round() as u8)
146}
147
148#[derive(Clone)]
187pub struct Map {
188 zoom: f32,
189 previous_zoom: f32,
190 points: Option<HashMap<usize, MapPoint>>,
191 segments: Option<rstar::RTree<MapSegment>>,
192 labels: Vec<MapLabel>,
193 tree: Option<KdTree<f32, usize, [f32; 2]>>,
194 visible_points: Vec<isize>,
195 map_area: Rect,
196 reference: MapBounds,
197 current: MapBounds,
198 current_index: usize,
199 notifications: HashMap<usize, Notification>,
200 node_states: HashMap<usize, NodeState>,
201 segment_notifications: HashMap<(usize, usize), SegmentNotification>,
202 segment_states: HashMap<(usize, usize), SegmentState>,
203 segment_ids: HashSet<(usize, usize)>,
207 min_size: (Option<f32>, Option<f32>),
208 max_size: (Option<f32>, Option<f32>),
209 pub settings: MapSettings,
212 menu_manager: Option<Rc<dyn ContextMenuManager>>,
213 node_template: Option<Rc<dyn NodeTemplate>>,
214 segment_template: Option<Rc<dyn SegmentTemplate>>,
215 markers: HashMap<usize, usize>,
216 theme: Rc<dyn MapTheme>,
218}
219
220#[derive(Clone, Copy, Debug)]
222struct Notification {
223 started: Instant,
224 animation: NodeAnimation,
225 color: Option<Color32>,
227}
228
229#[derive(Clone, Copy, Debug)]
231struct NodeState {
232 animation: SteadyAnimation,
233 color: Option<Color32>,
235}
236
237#[derive(Clone, Copy, Debug)]
239struct SegmentNotification {
240 started: Instant,
241 animation: SegmentAnimation,
242 color: Option<Color32>,
244}
245
246#[derive(Clone, Copy, Debug)]
248struct SegmentState {
249 animation: SteadySegmentAnimation,
250 color: Option<Color32>,
252}
253
254pub struct NodeHandle<'a> {
274 map: &'a mut Map,
275 id: usize,
276 color: Option<Color32>,
277}
278
279impl NodeHandle<'_> {
280 pub fn color(mut self, color: Color32) -> Self {
284 self.color = Some(color);
285 self
286 }
287
288 fn notify_with(self, animation: NodeAnimation, at: Instant) {
289 self.map.notifications.insert(
290 self.id,
291 Notification {
292 started: at,
293 animation,
294 color: self.color,
295 },
296 );
297 }
298
299 fn set_state(self, animation: SteadyAnimation) {
300 self.map.node_states.insert(
301 self.id,
302 NodeState {
303 animation,
304 color: self.color,
305 },
306 );
307 }
308
309 pub fn pulse(self, at: Instant) {
311 self.notify_with(NodeAnimation::Pulse, at);
312 }
313
314 pub fn ripple(self, at: Instant) {
316 self.notify_with(NodeAnimation::Ripple, at);
317 }
318
319 pub fn countdown(self, at: Instant) {
321 self.notify_with(NodeAnimation::CountdownArc, at);
322 }
323
324 pub fn scale_in(self, at: Instant) {
326 self.notify_with(NodeAnimation::ScaleIn, at);
327 }
328
329 pub fn crosshair(self, at: Instant) {
331 self.notify_with(NodeAnimation::Crosshair, at);
332 }
333
334 pub fn halo(self) {
336 self.set_state(SteadyAnimation::Halo);
337 }
338
339 pub fn blink(self) {
341 self.set_state(SteadyAnimation::Blink);
342 }
343
344 pub fn orbit(self) {
346 self.set_state(SteadyAnimation::Orbit);
347 }
348
349 pub fn clear(self) {
351 self.map.notifications.remove(&self.id);
352 self.map.node_states.remove(&self.id);
353 }
354}
355
356pub struct SegmentHandle<'a> {
371 map: &'a mut Map,
372 id: (usize, usize),
373 color: Option<Color32>,
374}
375
376impl SegmentHandle<'_> {
377 pub fn color(mut self, color: Color32) -> Self {
381 self.color = Some(color);
382 self
383 }
384
385 pub fn flash(self, at: Instant) {
388 self.map.segment_notifications.insert(
389 self.id,
390 SegmentNotification {
391 started: at,
392 animation: SegmentAnimation::FlashDecay,
393 color: self.color,
394 },
395 );
396 }
397
398 pub fn comet_once(self, at: Instant, direction: CometDirection) {
403 self.map.segment_notifications.insert(
404 self.id,
405 SegmentNotification {
406 started: at,
407 animation: SegmentAnimation::Comet(direction),
408 color: self.color,
409 },
410 );
411 }
412
413 pub fn wipe(self, at: Instant) {
417 self.map.segment_notifications.insert(
418 self.id,
419 SegmentNotification {
420 started: at,
421 animation: SegmentAnimation::Wipe,
422 color: self.color,
423 },
424 );
425 }
426
427 pub fn comet(self) {
430 self.map.segment_states.insert(
431 self.id,
432 SegmentState {
433 animation: SteadySegmentAnimation::Comet,
434 color: self.color,
435 },
436 );
437 }
438
439 pub fn dash(self) {
442 self.map.segment_states.insert(
443 self.id,
444 SegmentState {
445 animation: SteadySegmentAnimation::Dash,
446 color: self.color,
447 },
448 );
449 }
450
451 pub fn glow_band(self) {
455 self.map.segment_states.insert(
456 self.id,
457 SegmentState {
458 animation: SteadySegmentAnimation::GlowBand,
459 color: self.color,
460 },
461 );
462 }
463
464 pub fn chevrons(self) {
467 self.map.segment_states.insert(
468 self.id,
469 SegmentState {
470 animation: SteadySegmentAnimation::Chevrons,
471 color: self.color,
472 },
473 );
474 }
475
476 pub fn clear(self) {
478 self.map.segment_notifications.remove(&self.id);
479 self.map.segment_states.remove(&self.id);
480 }
481}
482
483impl Default for Map {
484 fn default() -> Self {
486 Map::new()
487 }
488}
489
490impl Widget for &mut Map {
491 fn ui(self, ui: &mut egui::Ui) -> Response {
494 let rect = self.calculate_widget_dimensions(ui);
495
496 self.reference.dist = rect.distance();
498
499 self.assign_visual_style(ui);
500
501 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
502
503 let frame_margin = canvas.total_margin().sum();
513 let painter_size = (self.map_area.size() - frame_margin).max(Vec2::ZERO);
514
515 let inner_response = canvas.show(ui, |ui| {
516 let _span = tracing::info_span!("paint_map").entered();
517
518 if ui.is_rect_visible(self.map_area) {
519 let (resp, paint) =
520 ui.allocate_painter(painter_size, egui::Sense::click_and_drag());
521 let vec = resp.drag_delta();
522 if vec.length() != 0.0 {
523 let _span = tracing::info_span!("calculating_points_in_visible_area").entered();
524
525 let coords = RawPoint::from(vec.to_pos2());
526 let new_pos = self.reference.pos - (coords / self.zoom);
527 self.set_pos(new_pos.into());
528 }
529 if self.zoom < self.settings.line_visible_zoom {
530 let mut text_settings = TextSettings {
532 size: self.settings.label_text_size,
536 anchor: Align2::CENTER_CENTER,
537 family: FontFamily::Proportional,
538 text: String::new(),
539 position: RawPoint::default(),
540 text_color: ui.visuals().text_color(),
541 };
542 for label in &self.labels {
543 text_settings.text.clone_from(&label.text);
544 text_settings.position = RawPoint::from(label.center);
545 self.paint_label(&paint, &text_settings);
546 }
547 }
548
549 let rect_midpoint = RawPoint::from(resp.rect.center());
555 let min_point = self.current.pos - rect_midpoint;
556 let vec_points = &self.visible_points;
557 let hashm = &self.points;
558
559 let now = Instant::now();
563 self.notifications
564 .retain(|_, n| now.duration_since(n.started).as_secs_f32() < 10.0);
565 self.segment_notifications
566 .retain(|_, n| now.duration_since(n.started).as_secs_f32() < 10.0);
567
568 for segment in self.paint_map_lines(&paint, &min_point) {
569 self.segment_notifications.remove(&segment);
570 }
571
572 if let Ok(nodes_to_remove) =
573 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
574 {
575 for node in nodes_to_remove {
576 self.notifications.remove(&node);
577 }
578 }
579
580 for marker in &self.markers {
581 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
582 let adjusted_point = RawPoint::from(point.coords) * self.zoom - min_point;
583 if let Some(template) = &self.node_template {
584 template.marker_ui(
585 ui,
586 MarkerContext {
587 position: adjusted_point.into(),
588 zoom: self.zoom,
589 kind: self.settings.marker_animation,
590 node_id: *marker.1,
591 },
592 );
593 } else {
594 let color = if ui.visuals().dark_mode {
595 Color32::LIGHT_GREEN
596 } else {
597 Color32::GREEN
598 };
599 let time = ui.input(|i| i.time) as f32;
603 let effect = match self.settings.marker_animation {
604 SteadyAnimation::Blink => Animation::blink,
605 SteadyAnimation::Halo => Animation::halo,
606 SteadyAnimation::Orbit => Animation::orbit,
607 };
608 effect(ui.painter(), adjusted_point.into(), self.zoom, time, color);
609 ui.ctx().request_repaint();
611 }
612 }
613 }
614
615 self.paint_sub_components(ui, self.map_area);
616
617 self.capture_mouse_events(ui, &resp);
618
619 if self.zoom != self.previous_zoom {
620 let _span = tracing::info_span!("calculating viewport with zoom").entered();
621 self.adjust_bounds();
622 self.calculate_visible_points();
623 self.previous_zoom = self.zoom;
624 }
625
626 if let Some(menu_mon) = &mut self.menu_manager {
627 resp.context_menu(|ui| {
628 menu_mon.ui(ui);
629 });
630 }
631
632 #[cfg(feature = "debug_overlay")]
633 self.print_debug_info(ui, &resp);
634 }
635 });
636 inner_response.response
641 }
642}
643
644impl Map {
645 pub fn new() -> Self {
650 let settings = MapSettings::default();
651 Self {
652 zoom: 1.0,
653 previous_zoom: 1.0,
654 map_area: Rect::NOTHING,
655 tree: None,
656 points: None,
657 labels: Vec::new(),
658 visible_points: Vec::new(),
659 current: MapBounds::default(),
660 reference: MapBounds::default(),
661 settings,
662 min_size: (None, None),
663 max_size: (None, None),
664 current_index: 0,
665 notifications: HashMap::new(),
666 node_states: HashMap::new(),
667 segment_notifications: HashMap::new(),
668 segment_states: HashMap::new(),
669 segment_ids: HashSet::new(),
670 menu_manager: None,
671 node_template: None,
672 segment_template: None,
673 markers: HashMap::new(),
674 segments: None,
675 theme: Rc::new(Theme::default()),
676 }
677 }
678
679 fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
680 let available = ui.available_rect_before_wrap();
681 let mut size = available.size();
682 if let Some(max_width) = self.max_size.0 {
683 size.x = size.x.min(max_width);
684 }
685 if let Some(max_height) = self.max_size.1 {
686 size.y = size.y.min(max_height);
687 }
688 if let Some(min_width) = self.min_size.0 {
689 size.x = size.x.max(min_width);
690 }
691 if let Some(min_height) = self.min_size.1 {
692 size.y = size.y.max(min_height);
693 }
694 self.map_area = Rect::from_min_size(available.min, size);
695 RawLine::new(
696 RawPoint::from(self.map_area.left_top()),
697 RawPoint::from(self.map_area.right_bottom()),
698 )
699 }
700
701 fn calculate_visible_points(&mut self) {
702 let _span = tracing::info_span!("calculate_visible_points").entered();
703 if self.current.dist > 0.0
704 && self.current.dist < f32::INFINITY
705 && let Some(tree) = &self.tree
706 {
707 let center = self.current.pos / self.zoom;
708 let radius = self.current.dist.powi(2);
709 let point: [f32; 2] = center.into();
710 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
711 self.visible_points.clear();
712 for point in vis_pos {
713 self.visible_points.push(point.1.cast_signed());
714 }
715 }
716 }
717
718 pub fn add_points(&mut self, points: Vec<MapPoint>) {
746 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
747 let mut hash_map = HashMap::new();
748 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
749 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
750 for entry in points {
751 for i in 0..min.components.len() {
752 if entry.coords[i] < min.components[i] {
753 min.components[i] = entry.coords[i];
754 }
755 if entry.coords[i] > max.components[i] {
756 max.components[i] = entry.coords[i];
757 }
758 }
759 let _result = tree.add(entry.coords, entry.get_id());
760 hash_map.insert(entry.get_id(), entry);
761 }
762 self.reference.min = min;
764 self.reference.max = max;
765 self.points = Some(hash_map);
766 self.tree = Some(tree);
767 self.reference.pos = RawLine::new(min, max).midpoint();
768 if self.map_area.area() == 0.0 {
772 self.reference.dist = 3000.00;
773 } else {
774 let rect = RawLine::new(
775 RawPoint::from(self.map_area.left_top()),
776 RawPoint::from(self.map_area.right_bottom()),
777 );
778 self.reference.dist = rect.distance();
779 }
780 self.current = self.reference.clone();
781 self.calculate_visible_points();
782 }
783
784 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
814 let _span = tracing::info_span!("add_hashmap_points").entered();
815 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
816 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
817 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
818
819 for entry in hash_map.iter() {
820 for i in 0..min.components.len() {
821 if entry.1.coords[i] < min.components[i] {
822 min.components[i] = entry.1.coords[i];
823 }
824 if entry.1.coords[i] > max.components[i] {
825 max.components[i] = entry.1.coords[i];
826 }
827 }
828 let _result = tree.add(entry.1.coords, *entry.0);
829 }
830
831 self.reference.min = min;
833 self.reference.max = max;
834 self.points = Some(hash_map);
835 self.tree = Some(tree);
836 self.reference.pos = RawLine::new(min, max).midpoint();
837 if self.map_area.area() == 0.0 {
841 self.reference.dist = 3000.00;
842 } else {
843 let rect = RawLine::new(
844 RawPoint::from(self.map_area.left_top()),
845 RawPoint::from(self.map_area.right_bottom()),
846 );
847 self.reference.dist = rect.distance();
848 }
849 self.current = self.reference.clone();
850 self.calculate_visible_points();
851 }
852
853 pub fn set_pos_from_nodeid(&mut self, node_id: usize) -> bool {
881 let _span = tracing::info_span!("set_pos_from_nodeid").entered();
882 if let Some(hash_map) = &self.points
883 && let Some(map_point) = hash_map.get(&node_id)
884 {
885 self.reference.pos = RawPoint::from(map_point.coords);
886 self.adjust_bounds();
887 self.calculate_visible_points();
888 true
889 } else {
890 tracing::warn!(
891 node_id,
892 loaded_nodes = self.points.as_ref().map_or(0, |p| p.len()),
893 "set_pos_from_nodeid: unknown node id, the view was left unchanged"
894 );
895 false
896 }
897 }
898
899 pub fn set_pos(&mut self, position: [f32; 2]) {
901 let _span = tracing::info_span!("set_pos").entered();
902 let point = RawPoint::from(position);
903 self.reference.pos = point;
904 self.adjust_bounds();
905 self.calculate_visible_points();
906 }
907
908 pub fn get_pos(&self) -> [f32; 2] {
910 let _span = tracing::info_span!("get_pos").entered();
911 self.reference.pos.into()
912 }
913
914 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
919 let _span = tracing::info_span!("add_labels").entered();
920 self.labels = labels;
921 }
922
923 pub fn add_lines(&mut self, segments: Vec<MapSegment>) {
935 let _span = tracing::info_span!("add_lines").entered();
936 self.segment_ids = segments.iter().map(|s| s.id).collect();
941 self.segments = Some(rstar::RTree::bulk_load(segments));
942 }
943
944 pub fn add_hashmap_lines(&mut self, segments: HashMap<(usize, usize), MapSegment>) {
953 let _span = tracing::info_span!("add_hashmap_lines").entered();
954 let segments: Vec<MapSegment> = segments.into_values().collect();
955 self.segment_ids = segments.iter().map(|s| s.id).collect();
956 self.segments = Some(rstar::RTree::bulk_load(segments));
957 }
958
959 fn adjust_bounds(&mut self) {
960 let _span = tracing::info_span!("adjust_bounds").entered();
961 self.current.max = self.reference.max * self.zoom;
962 self.current.min = self.reference.min * self.zoom;
963 self.current.dist = self.reference.dist / self.zoom;
964 self.current.pos = self.reference.pos * self.zoom;
965 }
966
967 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
968 let _span = tracing::info_span!("capture_mouse_events").entered();
969 if ui.rect_contains_pointer(self.map_area) {
971 ui.input(|x| {
972 let _span = tracing::info_span!("capture_mouse_events_input").entered();
973
974 if !x.events.is_empty() {
975 for event in &x.events {
976 match event {
977 Event::MouseWheel {
978 unit: _,
979 delta,
980 modifiers,
981 phase: _,
982 } => {
983 #[cfg(target_os = "macos")]
984 let zoom_modifier = if modifiers.mac_cmd {
985 delta.y / 80.00
986 } else {
987 delta.y / 400.00
988 };
989
990 #[cfg(not(target_os = "macos"))]
991 let zoom_modifier = if modifiers.ctrl {
992 delta.y / 8.00
993 } else {
994 delta.y / 40.00
995 };
996
997 let mut pre_zoom = self.zoom + zoom_modifier;
998 if pre_zoom > self.settings.max_zoom {
999 pre_zoom = self.settings.max_zoom;
1000 }
1001 if pre_zoom < self.settings.min_zoom {
1002 pre_zoom = self.settings.min_zoom;
1003 }
1004 self.zoom = pre_zoom;
1005 }
1006 _ => {
1007 continue;
1008 }
1009 };
1010 }
1011 }
1012 });
1013 }
1014 }
1015
1016 pub fn set_zoom(&mut self, value: f32) {
1021 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
1022 self.zoom = value;
1023 }
1024 }
1025
1026 pub fn get_zoom(&mut self) -> f32 {
1028 self.zoom
1029 }
1030
1031 fn current_style(&self) -> &Style {
1034 self.settings
1035 .styles
1036 .get(self.current_index)
1037 .or(self.settings.styles.first())
1038 .expect("MapSettings::styles must not be empty")
1039 }
1040
1041 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
1042 let style_index = ui_obj.visuals().dark_mode as usize;
1043
1044 if self.current_index != style_index {
1045 let _span = tracing::info_span!("asign_visual_style").entered();
1046
1047 self.current_index = style_index;
1048 self.apply_theme_colors(style_index);
1049 let map_style = self.settings.styles.get_mut(style_index).unwrap();
1050 let visuals = &ui_obj.style().visuals;
1051 map_style.background_color = visuals.extreme_bg_color;
1052 map_style.border = Some(visuals.window_stroke);
1053 }
1054 }
1055
1056 fn apply_theme_colors(&mut self, index: usize) {
1061 let mode = if index == 1 {
1062 ColorMode::Dark
1063 } else {
1064 ColorMode::Light
1065 };
1066 let colors = self.theme.colors(mode);
1067 if let Some(map_style) = self.settings.styles.get_mut(index) {
1068 map_style.fill_color = colors.node;
1069 map_style.text_color = colors.text;
1070 map_style.alert_color = colors.alert;
1071 if let Some(line) = map_style.line.as_mut() {
1072 line.color = colors.segment;
1073 }
1074 }
1075 }
1076
1077 #[cfg(feature = "debug_overlay")]
1086 fn print_debug_info(&mut self, ui: &mut Ui, resp: &Response) {
1087 let _span = tracing::info_span!("printing debug data").entered();
1088
1089 let p = |v: f32| format!("{v:.2}");
1090 let mut rows: Vec<(String, Color32)> = vec![
1091 (
1092 format!(
1093 "MIN {}, {}",
1094 p(self.current.min.components[0]),
1095 p(self.current.min.components[1])
1096 ),
1097 Color32::LIGHT_GREEN,
1098 ),
1099 (
1100 format!(
1101 "MAX {}, {}",
1102 p(self.current.max.components[0]),
1103 p(self.current.max.components[1])
1104 ),
1105 Color32::LIGHT_GREEN,
1106 ),
1107 (
1108 format!(
1109 "CUR {}, {}",
1110 p(self.current.pos.components[0]),
1111 p(self.current.pos.components[1])
1112 ),
1113 Color32::LIGHT_GREEN,
1114 ),
1115 (
1116 format!("DST {}", p(self.current.dist)),
1117 Color32::LIGHT_GREEN,
1118 ),
1119 (format!("ZOM {}", self.zoom), Color32::GREEN),
1120 (
1121 format!(
1122 "REC {}, {} .. {}, {}",
1123 p(self.map_area.left_top().x),
1124 p(self.map_area.left_top().y),
1125 p(self.map_area.right_bottom().x),
1126 p(self.map_area.right_bottom().y)
1127 ),
1128 Color32::LIGHT_GREEN,
1129 ),
1130 ];
1131 if let Some(points) = &self.points {
1132 rows.push((format!("NUM {}", points.len()), Color32::LIGHT_GREEN));
1133 }
1134 if !self.visible_points.is_empty() {
1135 rows.push((
1136 format!("VIS {}", self.visible_points.len()),
1137 Color32::LIGHT_GREEN,
1138 ));
1139 }
1140 if let Some(pointer_pos) = resp.hover_pos() {
1141 rows.push((
1142 format!("HVR {}, {}", p(pointer_pos.x), p(pointer_pos.y)),
1143 Color32::LIGHT_BLUE,
1144 ));
1145 }
1146 let drag = resp.drag_delta();
1147 if drag.length() != 0.0 {
1148 rows.push((format!("DRG {}, {}", p(drag.x), p(drag.y)), Color32::GOLD));
1149 }
1150
1151 let overlay_rect = Rect::from_min_max(
1161 self.map_area.left_top() + Vec2::new(6.0, 6.0),
1162 self.map_area.right_bottom(),
1163 );
1164 let mut overlay_ui = ui.new_child(
1165 UiBuilder::new()
1166 .max_rect(overlay_rect)
1167 .layout(Layout::top_down(Align::Min)),
1168 );
1169 CollapsingHeader::new(RichText::new("dbg").monospace().small().weak())
1172 .id_salt("egui_map_debug_overlay")
1173 .default_open(false)
1174 .show_background(false)
1175 .show(&mut overlay_ui, |ui| {
1176 for (text, color) in rows {
1177 ui.label(RichText::new(text).monospace().small().color(color));
1178 }
1179 });
1180 }
1181
1182 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
1183 let _span = tracing::info_span!("map_ui_paint_sub_components").entered();
1184 let zoom_slider = egui::Slider::new(
1185 &mut self.zoom,
1186 self.settings.min_zoom..=self.settings.max_zoom,
1187 )
1188 .show_value(false)
1189 .orientation(SliderOrientation::Vertical);
1190 let mut pos1 = rect.right_top();
1191 let mut pos2 = rect.right_top();
1192 pos1.x -= 80.0;
1193 pos1.y += 120.0;
1194 pos2.x -= 60.0;
1195 pos2.y += 240.0;
1196
1197 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
1198 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
1199 ui_obj.scope_builder(ui_builder, |ui_obj| {
1200 ui_obj.add(zoom_slider);
1201 });
1202 }
1203
1204 fn paint_map_points(
1205 &self,
1206 vec_points: &Vec<isize>,
1207 hashm: &Option<HashMap<usize, MapPoint>>,
1208 paint: &Painter,
1209 ui_obj: &mut Ui,
1210 min_point: &RawPoint,
1211 resp: &Response,
1212 ) -> Result<Vec<usize>, ()> {
1213 let mut nearest_id = None;
1214 let mut nodes_to_remove = Vec::new();
1215 let mut shape_vec = vec![];
1216
1217 if hashm.is_none() {
1218 return Err(());
1219 }
1220 if vec_points.is_empty() {
1221 return Err(());
1222 }
1223 if self.settings.node_text_visibility == VisibilitySetting::Hover
1225 && resp.hovered()
1226 && let Some(point) = resp.hover_pos()
1227 {
1228 let raw_point = RawPoint::from(point);
1229 let hovered_map_point = (*min_point + raw_point) / self.zoom;
1230 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
1231 &hovered_map_point.components,
1232 1,
1233 &squared_euclidean,
1234 ) {
1235 nearest_id = Some(nearest_node.first().unwrap().1);
1236 }
1237 }
1238 let mut text_settings = TextSettings {
1240 size: self.settings.node_text_size,
1244 anchor: Align2::LEFT_BOTTOM,
1245 family: FontFamily::Proportional,
1246 text: String::new(),
1247 position: RawPoint::default(),
1248 text_color: ui_obj.visuals().text_color(),
1249 };
1250
1251 for temp_point in vec_points {
1253 let parsed_point = temp_point.cast_unsigned();
1254 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
1255 let _span = tracing::info_span!("painting_points_m").entered();
1256 let viewport_point = RawPoint::from(system.coords) * self.zoom - min_point;
1257 if let Some(node_template) = &self.node_template {
1258 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
1259 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
1260 }
1261 } else if self.zoom > self.settings.label_visible_zoom
1262 && self.settings.node_text_visibility == VisibilitySetting::Always
1263 || (self.settings.node_text_visibility == VisibilitySetting::Hover
1264 && nearest_id.unwrap_or(&0usize) == &system.get_id())
1265 {
1266 let mut viewport_text = viewport_point;
1267 viewport_text.components[0] += 3.0 * self.zoom;
1268 viewport_text.components[1] -= 3.0 * self.zoom;
1269 text_settings.position = viewport_text;
1270 text_settings.text = system.get_name();
1271 self.paint_label(paint, &text_settings);
1272 }
1273
1274 let system_id = system.get_id();
1275
1276 if let Some(state) = self.node_states.get(&system_id) {
1279 let color = state.color.unwrap_or(self.current_style().alert_color);
1280 if let Some(template) = &self.node_template {
1281 template.marker_ui(
1288 ui_obj,
1289 MarkerContext {
1290 position: viewport_point.into(),
1291 zoom: self.zoom,
1292 kind: state.animation,
1293 node_id: system_id,
1294 },
1295 );
1296 } else {
1297 let effect = match state.animation {
1298 SteadyAnimation::Blink => Animation::blink,
1299 SteadyAnimation::Halo => Animation::halo,
1300 SteadyAnimation::Orbit => Animation::orbit,
1301 };
1302 let time = ui_obj.input(|i| i.time) as f32;
1305 effect(paint, viewport_point.into(), self.zoom, time, color);
1306 }
1307 ui_obj.ctx().request_repaint();
1309 }
1310
1311 if let Some(notification) = self.notifications.get(&system_id) {
1312 let color = notification
1313 .color
1314 .unwrap_or(self.current_style().alert_color);
1315 if let Some(template) = &self.node_template {
1316 template.notification_ui(
1317 ui_obj,
1318 NotificationContext {
1319 position: viewport_point.into(),
1320 zoom: self.zoom,
1321 initial_time: notification.started,
1322 color,
1323 kind: notification.animation,
1324 node_id: system_id,
1325 },
1326 );
1327 } else {
1328 let effect = match notification.animation {
1329 NodeAnimation::Pulse => Animation::pulse,
1330 NodeAnimation::Ripple => Animation::ripple,
1331 NodeAnimation::CountdownArc => Animation::countdown_arc,
1332 NodeAnimation::ScaleIn => Animation::scale_in,
1333 NodeAnimation::Crosshair => Animation::crosshair,
1334 };
1335 if effect(
1336 paint,
1337 viewport_point.into(),
1338 self.zoom,
1339 notification.started,
1340 color,
1341 ) {
1342 ui_obj.ctx().request_repaint();
1343 } else {
1344 nodes_to_remove.push(system_id);
1345 }
1346 }
1347 }
1348 if let Some(node_template) = &self.node_template {
1349 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
1350 } else {
1351 shape_vec.push(Shape::circle_filled(
1352 viewport_point.into(),
1353 4.00 * self.zoom,
1354 system.color.unwrap_or(self.current_style().fill_color),
1355 ));
1356 }
1357 }
1358 }
1359 paint.extend(shape_vec);
1360 Ok(nodes_to_remove)
1361 }
1362
1363 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) -> Vec<(usize, usize)> {
1367 let _span = tracing::info_span!("paint_map_lines").entered();
1368 let mut segments_to_remove = Vec::new();
1369
1370 if self.zoom <= self.settings.line_visible_zoom {
1371 return segments_to_remove;
1372 }
1373 let Some(segments) = &self.segments else {
1374 return segments_to_remove;
1375 };
1376
1377 let line_fade = ((self.zoom - self.settings.line_visible_zoom) / 0.80).clamp(0.0, 1.0);
1384
1385 let default_stroke = self.current_style().line.map(|stroke| {
1390 if line_fade >= 1.0 {
1391 stroke
1392 } else {
1393 let mut tup_stroke = stroke.color.to_tuple();
1394 tup_stroke.3 = (255.0 * line_fade).round() as u8;
1395 let color = Color32::from_rgba_unmultiplied(
1396 tup_stroke.0,
1397 tup_stroke.1,
1398 tup_stroke.2,
1399 tup_stroke.3,
1400 );
1401 Stroke::new(stroke.width, color)
1402 }
1403 });
1404
1405 let center = self.current.pos / self.zoom;
1409 let padding = default_stroke.map(|s| s.width).unwrap_or(0.0) / self.zoom;
1410 let half = RawPoint::new(
1411 self.map_area.width() / 2.0 / self.zoom + padding,
1412 self.map_area.height() / 2.0 / self.zoom + padding,
1413 );
1414 let query = rstar::AABB::from_corners((center - half).into(), (center + half).into());
1415
1416 let effect_fade = (line_fade + SEGMENT_EFFECT_ALPHA_BOOST).min(1.0);
1421
1422 for segment in segments.locate_in_envelope_intersecting(query) {
1423 let raw_line = segment.raw_line();
1424 let pos_a: Pos2 = (raw_line.points[0] * self.zoom - min_point).into();
1425 let pos_b: Pos2 = (raw_line.points[1] * self.zoom - min_point).into();
1426
1427 if let Some(template) = &self.segment_template {
1435 template.segment_ui(painter, pos_a, pos_b, self.zoom, segment);
1436 } else if let Some(stroke) = default_stroke {
1437 painter.add(Shape::line_segment([pos_a, pos_b], stroke));
1438 }
1439
1440 if let Some(state) = self.segment_states.get(&segment.id) {
1444 let color = scale_alpha(
1445 state.color.unwrap_or(self.current_style().alert_color),
1446 effect_fade,
1447 );
1448 if let Some(template) = &self.segment_template {
1449 let time = painter.ctx().input(|i| i.time) as f32;
1450 template.segment_state_ui(painter, pos_a, pos_b, self.zoom, time, color);
1451 } else {
1452 let effect = match state.animation {
1453 SteadySegmentAnimation::Comet => Animation::comet,
1454 SteadySegmentAnimation::Dash => Animation::dash,
1455 SteadySegmentAnimation::GlowBand => Animation::glow_band,
1456 SteadySegmentAnimation::Chevrons => Animation::chevrons,
1457 };
1458 let time = painter.ctx().input(|i| i.time) as f32;
1459 effect(painter, pos_a, pos_b, self.zoom, time, color);
1460 }
1461 painter.ctx().request_repaint();
1463 }
1464
1465 if let Some(notification) = self.segment_notifications.get(&segment.id) {
1466 let color = scale_alpha(
1467 notification
1468 .color
1469 .unwrap_or(self.current_style().alert_color),
1470 effect_fade,
1471 );
1472 let still_playing = if let Some(template) = &self.segment_template {
1473 template.segment_notification_ui(
1474 painter,
1475 pos_a,
1476 pos_b,
1477 self.zoom,
1478 notification.started,
1479 color,
1480 )
1481 } else {
1482 match notification.animation {
1483 SegmentAnimation::FlashDecay => Animation::flash_decay(
1484 painter,
1485 pos_a,
1486 pos_b,
1487 self.zoom,
1488 notification.started,
1489 color,
1490 ),
1491 SegmentAnimation::Comet(direction) => Animation::comet_once(
1492 painter,
1493 pos_a,
1494 pos_b,
1495 self.zoom,
1496 notification.started,
1497 color,
1498 direction,
1499 ),
1500 SegmentAnimation::Wipe => Animation::wipe(
1501 painter,
1502 pos_a,
1503 pos_b,
1504 self.zoom,
1505 notification.started,
1506 color,
1507 ),
1508 }
1509 };
1510 if still_playing {
1511 painter.ctx().request_repaint();
1512 } else {
1513 segments_to_remove.push(segment.id);
1514 }
1515 }
1516 }
1517 segments_to_remove
1518 }
1519
1520 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
1521 let _span = tracing::info_span!("paint_label").entered();
1522 paint.text(
1523 text_settings.position.into(),
1524 text_settings.anchor,
1525 text_settings.text.clone(),
1526 FontId::new(text_settings.size, text_settings.family.clone()),
1527 text_settings.text_color,
1528 );
1529 }
1530
1531 #[deprecated(
1554 since = "0.4.0",
1555 note = "use `map.node(id)` and pick an effect, e.g. `if let Some(n) = map.node(id) { n.pulse(time) }`"
1556 )]
1557 pub fn notify(&mut self, id_node: usize, time: Instant) {
1558 let _span = tracing::info_span!("notify").entered();
1559 self.notifications.insert(
1560 id_node,
1561 Notification {
1562 started: time,
1563 animation: NodeAnimation::Pulse,
1564 color: None,
1565 },
1566 );
1567 }
1568
1569 pub fn node(&mut self, id: usize) -> Option<NodeHandle<'_>> {
1598 if !self
1599 .points
1600 .as_ref()
1601 .is_some_and(|points| points.contains_key(&id))
1602 {
1603 return None;
1604 }
1605 Some(NodeHandle {
1606 map: self,
1607 id,
1608 color: None,
1609 })
1610 }
1611
1612 pub fn segment(&mut self, id: (usize, usize)) -> Option<SegmentHandle<'_>> {
1639 if !self.segment_ids.contains(&id) {
1640 return None;
1641 }
1642 Some(SegmentHandle {
1643 map: self,
1644 id,
1645 color: None,
1646 })
1647 }
1648
1649 pub fn line_at(&self, point: [f32; 2], tolerance: f32) -> Option<(usize, usize)> {
1663 let _span = tracing::info_span!("line_at").entered();
1664 let segments = self.segments.as_ref()?;
1665 let tolerance = tolerance.max(0.0);
1666
1667 let center = RawPoint::from(point);
1668 let padding = RawPoint::new(tolerance, tolerance);
1669 let query = rstar::AABB::from_corners((center - padding).into(), (center + padding).into());
1670
1671 let mut closest: Option<(f32, (usize, usize))> = None;
1672 for segment in segments.locate_in_envelope_intersecting(query) {
1673 let distance = segment.raw_line().distance_to_point(center);
1674 if distance <= tolerance && closest.as_ref().is_none_or(|(best, _)| distance < *best) {
1675 closest = Some((distance, segment.id));
1676 }
1677 }
1678 closest.map(|(_, id)| id)
1679 }
1680
1681 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
1684 self.menu_manager = Some(manager);
1685 }
1686
1687 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
1695 self.node_template = Some(template);
1696 }
1697
1698 pub fn set_segment_template(&mut self, template: Rc<dyn SegmentTemplate>) {
1704 self.segment_template = Some(template);
1705 }
1706
1707 pub fn set_theme(&mut self, new_theme: Rc<dyn MapTheme>) {
1716 self.theme = new_theme;
1717 for index in 0..self.settings.styles.len().min(2) {
1718 self.apply_theme_colors(index);
1719 }
1720 }
1721
1722 pub fn update_marker(&mut self, id: usize, node_id: usize) {
1727 self.markers
1728 .entry(id)
1729 .and_modify(|value| *value = node_id)
1730 .or_insert(node_id);
1731 }
1732
1733 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
1736 self.min_size = (width, height);
1737 }
1738
1739 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
1742 self.max_size = (width, height);
1743 }
1744}
1745
1746#[cfg(test)]
1747mod tests {
1748 use super::*;
1749 use std::time::Duration;
1750
1751 fn sample_points() -> Vec<MapPoint> {
1752 let mut map = Vec::new();
1753 map.push(MapPoint::new(1, [0.0, 0.0]));
1754 map.push(MapPoint::new(2, [10.0, 10.0]));
1755 map.push(MapPoint::new(3, [-10.0, -10.0]));
1756 map
1757 }
1758
1759 #[test]
1762 fn map_new_initial_state() {
1763 let map = Map::new();
1764 assert_eq!(map.zoom, 1.0);
1765 assert_eq!(map.previous_zoom, 1.0);
1766 assert!(map.points.is_none());
1767 assert!(map.segments.is_none());
1768 assert!(map.tree.is_none());
1769 assert!(map.labels.is_empty());
1770 assert!(map.visible_points.is_empty());
1771 assert!(map.markers.is_empty());
1772 assert!(map.notifications.is_empty());
1773 assert!(map.node_states.is_empty());
1774 assert!(map.segment_notifications.is_empty());
1775 assert!(map.segment_states.is_empty());
1776 assert!(map.segment_ids.is_empty());
1777 assert_eq!(map.min_size, (None, None));
1778 assert_eq!(map.max_size, (None, None));
1779 assert_eq!(map.current_index, 0);
1780 }
1781
1782 #[test]
1783 fn map_default_equals_new() {
1784 let map = Map::default();
1785 assert_eq!(map.zoom, 1.0);
1786 assert!(map.points.is_none());
1787 }
1788
1789 #[test]
1792 fn set_zoom_within_range() {
1793 let mut map = Map::new();
1794 map.set_zoom(1.5);
1795 assert_eq!(map.get_zoom(), 1.5);
1796 }
1797
1798 #[test]
1799 fn set_zoom_at_exact_limits() {
1800 let mut map = Map::new();
1801 map.set_zoom(map.settings.min_zoom);
1802 assert_eq!(map.get_zoom(), 0.1);
1803 map.set_zoom(map.settings.max_zoom);
1804 assert_eq!(map.get_zoom(), 2.0);
1805 }
1806
1807 #[test]
1808 fn set_zoom_out_of_range_is_ignored() {
1809 let mut map = Map::new();
1810 let initial = map.get_zoom();
1811 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
1813 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
1815 }
1816
1817 #[test]
1820 fn add_hashmap_points_computes_bounds() {
1821 let mut map = Map::new();
1822 map.add_points(sample_points());
1823
1824 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
1825 assert_eq!(map.reference.max.components, [10.0, 10.0]);
1826 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1828 assert_eq!(map.reference.dist, 3000.0);
1830 assert_eq!(map.current.min.components, map.reference.min.components);
1832 assert_eq!(map.current.max.components, map.reference.max.components);
1833 assert_eq!(map.current.pos.components, map.reference.pos.components);
1834 assert_eq!(map.current.dist, map.reference.dist);
1835 assert!(map.points.is_some());
1836 assert!(map.tree.is_some());
1837 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1838 }
1839
1840 #[test]
1841 fn add_hashmap_points_populates_visible_points() {
1842 let mut map = Map::new();
1843 map.add_points(sample_points());
1844 assert_eq!(map.visible_points.len(), 3);
1846 }
1847
1848 fn render_line_segments(map: &mut Map) -> Vec<[egui::Pos2; 2]> {
1851 use egui::{Context, RawInput, Shape};
1852 let ctx = Context::default();
1853 let input = RawInput {
1854 screen_rect: Some(egui::Rect::from_min_size(
1855 egui::Pos2::ZERO,
1856 egui::vec2(500.0, 500.0),
1857 )),
1858 ..RawInput::default()
1859 };
1860 let mut output = ctx.run_ui(input, |ui| {
1861 ui.add(&mut *map);
1862 });
1863 output.textures_delta.clear();
1869 output
1870 .shapes
1871 .iter()
1872 .filter_map(|cs| match cs.shape {
1873 Shape::LineSegment { points, .. } => Some(points),
1874 _ => None,
1875 })
1876 .collect()
1877 }
1878
1879 #[test]
1880 fn segment_crossing_viewport_is_painted_even_with_far_endpoints() {
1881 let mut map = Map::new();
1886 map.set_zoom(1.0);
1887 let mut lines = Vec::new();
1888 lines.push(MapSegment::new((1, 2), [-4000.0, -1.0], [4000.0, 1.0]));
1889 map.add_lines(lines);
1890 map.set_pos([0.0, 0.0]);
1891
1892 let segments = render_line_segments(&mut map);
1893 assert_eq!(segments.len(), 1);
1894 }
1895
1896 #[test]
1897 fn segment_outside_viewport_is_not_painted() {
1898 let mut map = Map::new();
1899 map.set_zoom(1.0);
1900 let mut lines = Vec::new();
1901 lines.push(MapSegment::new(
1902 (1, 2),
1903 [10_000.0, 10_000.0],
1904 [10_100.0, 10_100.0],
1905 ));
1906 map.add_lines(lines);
1907 map.set_pos([0.0, 0.0]);
1908
1909 assert!(render_line_segments(&mut map).is_empty());
1910 }
1911
1912 #[test]
1913 fn add_lines_builds_segment_tree() {
1914 let mut map = Map::new();
1915 map.add_points(sample_points());
1916 let mut lines = Vec::new();
1917 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1918 map.add_lines(lines);
1919
1920 let tree = map
1921 .segments
1922 .as_ref()
1923 .expect("add_lines must build the segment tree");
1924 assert_eq!(tree.size(), 1);
1925
1926 let hit_query = rstar::AABB::from_corners([-1.0, -1.0], [1.0, 1.0]);
1929 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1930 assert_eq!(hits.len(), 1);
1931 assert_eq!(hits[0].id, (1, 2));
1932
1933 let miss_query = rstar::AABB::from_corners([100.0, 100.0], [200.0, 200.0]);
1934 assert_eq!(tree.locate_in_envelope_intersecting(miss_query).count(), 0);
1935 }
1936
1937 #[test]
1938 fn add_lines_populates_segment_ids() {
1939 let mut map = Map::new();
1940 map.add_lines(vec![
1941 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]),
1942 MapSegment::new((3, 4), [1.0, 1.0], [2.0, 2.0]),
1943 ]);
1944 assert!(map.segment_ids.contains(&(1, 2)));
1945 assert!(map.segment_ids.contains(&(3, 4)));
1946 assert_eq!(map.segment_ids.len(), 2);
1947
1948 map.add_hashmap_lines(HashMap::from([(
1950 (5, 6),
1951 MapSegment::new((5, 6), [0.0, 0.0], [1.0, 1.0]),
1952 )]));
1953 assert_eq!(map.segment_ids, HashSet::from([(5, 6)]));
1954 }
1955
1956 #[test]
1957 fn map_check_line_is_painted_on_first_frame() {
1958 use egui::{Context, RawInput, Shape};
1959
1960 let mut map = Map::new();
1962 map.set_zoom(1.0);
1963
1964 let mut point_a = MapPoint::new(0, [0.0, 0.0]);
1965 point_a.connections.push((0, 1));
1966 let mut point_b = MapPoint::new(1, [50.0, 50.0]);
1967 point_b.connections.push((0, 1));
1968
1969 let mut lines = Vec::new();
1970 lines.push(MapSegment::new((0, 1), point_a.coords, point_b.coords));
1971
1972 let mut points = Vec::new();
1973 points.push(point_a);
1974 points.push(point_b);
1975 map.add_points(points);
1977 map.add_lines(lines);
1978
1979 map.set_pos([25.0, 25.0]);
1980
1981 let ctx = Context::default();
1983 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1984 let input = RawInput {
1985 screen_rect: Some(screen),
1986 ..RawInput::default()
1987 };
1988
1989 let mut output1 = ctx.run_ui(input.clone(), |ui| {
1990 ui.add(&mut map);
1991 });
1992
1993 let segments1: Vec<[egui::Pos2; 2]> = output1
1994 .shapes
1995 .iter()
1996 .filter_map(|cs| match cs.shape {
1997 Shape::LineSegment { points, .. } => Some(points),
1998 _ => None,
1999 })
2000 .collect();
2001 output1.textures_delta.clear();
2006
2007 assert!(
2008 !segments1.is_empty(),
2009 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
2010 );
2011
2012 let expected_a = egui::pos2(225.0, 225.0);
2015 let expected_b = egui::pos2(275.0, 275.0);
2016 let tolerance = 2.0;
2017 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
2018 let d_a1 = p1.distance(expected_a);
2019 let d_b1 = p2.distance(expected_b);
2020 let d_a2 = p2.distance(expected_a);
2021 let d_b2 = p1.distance(expected_b);
2022 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
2023 });
2024 assert!(
2025 found_on_frame1,
2026 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
2027 segments1
2028 );
2029
2030 let mut output2 = ctx.run_ui(input, |ui| {
2032 ui.add(&mut map);
2033 });
2034
2035 let segments2: Vec<[egui::Pos2; 2]> = output2
2036 .shapes
2037 .iter()
2038 .filter_map(|cs| match cs.shape {
2039 Shape::LineSegment { points, .. } => Some(points),
2040 _ => None,
2041 })
2042 .collect();
2043 output2.textures_delta.clear();
2044
2045 assert_eq!(
2046 segments1.len(),
2047 segments2.len(),
2048 "Frame 2: expected {} line segments (no duplication across frames), got {}",
2049 segments1.len(),
2050 segments2.len()
2051 );
2052 }
2053
2054 #[test]
2057 fn set_pos_and_get_pos_roundtrip() {
2058 let mut map = Map::new();
2059 map.set_pos([25.0, -35.0]);
2060 assert_eq!(map.get_pos(), [25.0, -35.0]);
2061 }
2062
2063 #[test]
2064 fn set_pos_from_nodeid_with_valid_id() {
2065 let mut map = Map::new();
2066 map.add_points(sample_points());
2067 assert!(map.set_pos_from_nodeid(2));
2068 assert_eq!(map.get_pos(), [10.0, 10.0]);
2069 }
2070
2071 #[test]
2078 fn centered_node_is_painted_at_the_middle_of_the_drawable_area() {
2079 use egui::{Context, RawInput, Shape};
2080
2081 for screen_size in [egui::vec2(500.0, 500.0), egui::vec2(800.0, 400.0)] {
2082 for zoom in [1.0, 2.0] {
2083 let mut map = Map::new();
2084 map.set_zoom(zoom);
2085 map.add_points(vec![MapPoint::new(7, [123.0, 456.0])]);
2086 map.set_pos_from_nodeid(7);
2087
2088 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size);
2089 let ctx = Context::default();
2090 let mut widget_rect = egui::Rect::NOTHING;
2091 let mut output = ctx.run_ui(
2092 RawInput {
2093 screen_rect: Some(screen),
2094 ..RawInput::default()
2095 },
2096 |ui| {
2097 widget_rect = ui.add(&mut map).rect;
2098 },
2099 );
2100
2101 assert!(
2103 screen.contains_rect(widget_rect),
2104 "{screen_size:?} zoom {zoom}: widget rect {widget_rect:?} overflows {screen:?}"
2105 );
2106
2107 let (node_center, drawable) = output
2110 .shapes
2111 .iter()
2112 .find_map(|cs| match &cs.shape {
2113 Shape::Circle(circle) => Some((circle.center, cs.clip_rect)),
2114 _ => None,
2115 })
2116 .expect("the node must be painted");
2117
2118 assert!(
2119 node_center.distance(drawable.center()) < 0.5,
2120 "{screen_size:?} zoom {zoom}: node painted at {node_center:?} but the \
2121 drawable area {drawable:?} is centred at {:?}",
2122 drawable.center()
2123 );
2124
2125 output.textures_delta.clear();
2126 }
2127 }
2128 }
2129
2130 #[test]
2131 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
2132 let mut map = Map::new();
2133 map.add_points(sample_points());
2134 let before = map.reference.pos.components;
2135 assert!(!map.set_pos_from_nodeid(999));
2137 assert_eq!(map.reference.pos.components, before);
2138 }
2139
2140 #[test]
2141 fn set_pos_from_nodeid_without_points_does_nothing() {
2142 let mut map = Map::new();
2143 assert!(!map.set_pos_from_nodeid(1));
2144 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
2145 }
2146
2147 #[test]
2150 fn add_labels_stores_labels() {
2151 let mut map = Map::new();
2152 let label = MapLabel {
2153 text: "Region".to_string(),
2154 center: Pos2::new(1.0, 2.0),
2155 };
2156 map.add_labels(vec![label]);
2157 assert_eq!(map.labels.len(), 1);
2158 assert_eq!(map.labels[0].text, "Region");
2159 }
2160
2161 #[test]
2162 fn add_lines_stores_lines() {
2163 let mut map = Map::new();
2164 let mut lines = Vec::new();
2165 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [1.0, 1.0]));
2166 map.add_lines(lines);
2167 let tree = map.segments.as_ref().unwrap();
2168 assert_eq!(tree.size(), 1);
2169 assert_eq!(
2170 tree.locate_in_envelope_intersecting(rstar::AABB::from_corners(
2171 [-1.0, -1.0],
2172 [2.0, 2.0],
2173 ))
2174 .next()
2175 .unwrap()
2176 .id,
2177 (1, 2)
2178 );
2179 }
2180
2181 #[test]
2184 fn line_at_returns_closest_line_within_tolerance() {
2185 let mut map = Map::new();
2186 map.add_points(sample_points());
2187 let mut lines = Vec::new();
2188 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]));
2189 lines.push(MapSegment::new((3, 4), [20.0, -5.0], [20.0, 5.0]));
2190 map.add_lines(lines);
2191
2192 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
2194 assert_eq!(hit, (1, 2));
2195
2196 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
2198 assert_eq!(hit, (3, 4));
2199 }
2200
2201 #[test]
2202 fn line_at_returns_none_beyond_tolerance() {
2203 let mut map = Map::new();
2204 map.add_points(sample_points());
2205 let mut lines = Vec::new();
2206 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
2207 map.add_lines(lines);
2208
2209 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
2212 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
2213 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
2214 }
2215
2216 #[test]
2217 fn line_at_returns_none_without_lines() {
2218 let map = Map::new();
2219 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
2220 }
2221
2222 #[test]
2223 fn line_at_negative_tolerance_behaves_like_zero() {
2224 let mut map = Map::new();
2225 map.add_points(sample_points());
2226 let mut lines = Vec::new();
2227 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
2228 map.add_lines(lines);
2229
2230 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
2232 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
2233 }
2234
2235 #[test]
2238 #[allow(deprecated)]
2239 fn deprecated_notify_still_records_a_pulse() {
2240 let mut map = Map::new();
2241 let t1 = Instant::now();
2242 map.notify(5, t1);
2243 let recorded = map.notifications.get(&5).expect("notify must record");
2244 assert_eq!(recorded.started, t1);
2245 assert_eq!(recorded.animation, NodeAnimation::Pulse);
2246 assert_eq!(recorded.color, None);
2247
2248 let t2 = t1 + Duration::from_secs(1);
2249 map.notify(5, t2);
2250 assert_eq!(map.notifications.get(&5).unwrap().started, t2);
2251 assert_eq!(map.notifications.len(), 1);
2252 }
2253
2254 fn map_with_nodes() -> Map {
2257 let mut map = Map::new();
2258 map.add_points(vec![
2259 MapPoint::new(1, [0.0, 0.0]),
2260 MapPoint::new(2, [10.0, 10.0]),
2261 ]);
2262 map
2263 }
2264
2265 #[test]
2266 fn node_returns_none_for_an_unknown_id() {
2267 let mut map = map_with_nodes();
2268 assert!(map.node(1).is_some());
2269 assert!(map.node(999).is_none());
2270 assert!(Map::new().node(1).is_none());
2272 }
2273
2274 #[test]
2275 fn each_event_effect_records_its_own_animation() {
2276 let now = Instant::now();
2277 for (apply, expected) in [
2278 (
2279 Box::new(|n: NodeHandle| n.pulse(now)) as Box<dyn FnOnce(NodeHandle)>,
2280 NodeAnimation::Pulse,
2281 ),
2282 (
2283 Box::new(|n: NodeHandle| n.ripple(now)),
2284 NodeAnimation::Ripple,
2285 ),
2286 (
2287 Box::new(|n: NodeHandle| n.countdown(now)),
2288 NodeAnimation::CountdownArc,
2289 ),
2290 (
2291 Box::new(|n: NodeHandle| n.scale_in(now)),
2292 NodeAnimation::ScaleIn,
2293 ),
2294 (
2295 Box::new(|n: NodeHandle| n.crosshair(now)),
2296 NodeAnimation::Crosshair,
2297 ),
2298 ] {
2299 let mut map = map_with_nodes();
2300 apply(map.node(1).unwrap());
2301 let recorded = map.notifications.get(&1).expect("effect must be recorded");
2302 assert_eq!(recorded.animation, expected);
2303 assert_eq!(recorded.started, now);
2304 assert!(map.node_states.is_empty());
2306 }
2307 }
2308
2309 #[test]
2310 fn each_steady_effect_records_lasting_state() {
2311 for (apply, expected) in [
2312 (
2313 Box::new(|n: NodeHandle| n.halo()) as Box<dyn FnOnce(NodeHandle)>,
2314 SteadyAnimation::Halo,
2315 ),
2316 (Box::new(|n: NodeHandle| n.blink()), SteadyAnimation::Blink),
2317 (Box::new(|n: NodeHandle| n.orbit()), SteadyAnimation::Orbit),
2318 ] {
2319 let mut map = map_with_nodes();
2320 apply(map.node(1).unwrap());
2321 assert_eq!(map.node_states.get(&1).unwrap().animation, expected);
2322 assert!(map.notifications.is_empty());
2324 }
2325 }
2326
2327 #[test]
2328 fn color_modifier_reaches_both_families() {
2329 let mut map = map_with_nodes();
2330 map.node(1)
2331 .unwrap()
2332 .color(Color32::RED)
2333 .pulse(Instant::now());
2334 map.node(2).unwrap().color(Color32::BLUE).halo();
2335
2336 assert_eq!(map.notifications.get(&1).unwrap().color, Some(Color32::RED));
2337 assert_eq!(map.node_states.get(&2).unwrap().color, Some(Color32::BLUE));
2338 }
2339
2340 #[test]
2341 fn a_node_can_carry_state_and_a_notification_at_once() {
2342 let mut map = map_with_nodes();
2343 map.node(1).unwrap().halo();
2344 map.node(1).unwrap().ripple(Instant::now());
2345
2346 assert!(map.node_states.contains_key(&1));
2347 assert!(map.notifications.contains_key(&1));
2348 }
2349
2350 #[test]
2351 fn clear_removes_both_families_for_that_node_only() {
2352 let mut map = map_with_nodes();
2353 map.node(1).unwrap().halo();
2354 map.node(1).unwrap().ripple(Instant::now());
2355 map.node(2).unwrap().halo();
2356
2357 map.node(1).unwrap().clear();
2358
2359 assert!(!map.node_states.contains_key(&1));
2360 assert!(!map.notifications.contains_key(&1));
2361 assert!(map.node_states.contains_key(&2), "node 2 must be untouched");
2362 }
2363
2364 #[test]
2365 fn re_triggering_replaces_the_previous_effect() {
2366 let mut map = map_with_nodes();
2367 map.node(1).unwrap().pulse(Instant::now());
2368 map.node(1).unwrap().crosshair(Instant::now());
2369
2370 assert_eq!(map.notifications.len(), 1);
2371 assert_eq!(
2372 map.notifications.get(&1).unwrap().animation,
2373 NodeAnimation::Crosshair
2374 );
2375 }
2376
2377 fn map_with_segments() -> Map {
2380 let mut map = Map::new();
2381 map.add_lines(vec![
2382 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]),
2383 MapSegment::new((3, 4), [0.0, 10.0], [10.0, 10.0]),
2384 ]);
2385 map
2386 }
2387
2388 #[test]
2389 fn segment_returns_none_for_an_unknown_id() {
2390 let mut map = map_with_segments();
2391 assert!(map.segment((1, 2)).is_some());
2392 assert!(map.segment((404, 404)).is_none());
2393 assert!(Map::new().segment((1, 2)).is_none());
2395 }
2396
2397 #[test]
2398 fn flash_records_a_segment_notification() {
2399 let mut map = map_with_segments();
2400 let now = Instant::now();
2401 map.segment((1, 2)).unwrap().flash(now);
2402
2403 let recorded = map
2404 .segment_notifications
2405 .get(&(1, 2))
2406 .expect("flash must be recorded");
2407 assert_eq!(recorded.animation, SegmentAnimation::FlashDecay);
2408 assert_eq!(recorded.started, now);
2409 assert!(map.segment_states.is_empty());
2411 }
2412
2413 #[test]
2414 fn each_event_segment_effect_records_its_own_notification() {
2415 for (apply, expected) in [
2416 (
2417 Box::new(|s: SegmentHandle, at: Instant| s.flash(at))
2418 as Box<dyn FnOnce(SegmentHandle, Instant)>,
2419 SegmentAnimation::FlashDecay,
2420 ),
2421 (
2422 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Forward)),
2423 SegmentAnimation::Comet(CometDirection::Forward),
2424 ),
2425 (
2426 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Reverse)),
2427 SegmentAnimation::Comet(CometDirection::Reverse),
2428 ),
2429 (
2430 Box::new(|s: SegmentHandle, at: Instant| s.wipe(at)),
2431 SegmentAnimation::Wipe,
2432 ),
2433 ] {
2434 let mut map = map_with_segments();
2435 let now = Instant::now();
2436 apply(map.segment((1, 2)).unwrap(), now);
2437
2438 let recorded = map
2439 .segment_notifications
2440 .get(&(1, 2))
2441 .expect("the effect must be recorded");
2442 assert_eq!(recorded.animation, expected);
2443 assert_eq!(recorded.started, now);
2444 assert!(map.segment_states.is_empty());
2446 }
2447 }
2448
2449 #[test]
2450 fn each_steady_segment_effect_records_lasting_state() {
2451 for (apply, expected) in [
2452 (
2453 Box::new(|s: SegmentHandle| s.comet()) as Box<dyn FnOnce(SegmentHandle)>,
2454 SteadySegmentAnimation::Comet,
2455 ),
2456 (
2457 Box::new(|s: SegmentHandle| s.dash()),
2458 SteadySegmentAnimation::Dash,
2459 ),
2460 (
2461 Box::new(|s: SegmentHandle| s.glow_band()),
2462 SteadySegmentAnimation::GlowBand,
2463 ),
2464 (
2465 Box::new(|s: SegmentHandle| s.chevrons()),
2466 SteadySegmentAnimation::Chevrons,
2467 ),
2468 ] {
2469 let mut map = map_with_segments();
2470 apply(map.segment((1, 2)).unwrap());
2471 assert_eq!(map.segment_states.get(&(1, 2)).unwrap().animation, expected);
2472 assert!(map.segment_notifications.is_empty());
2474 }
2475 }
2476
2477 #[test]
2478 fn steady_segment_effect_alpha_tracks_the_lines_zoom_fade_with_a_head_start() {
2479 let mut map = map_with_segments();
2486 map.set_zoom(0.6);
2487 let color = Color32::from_rgba_unmultiplied(10, 20, 30, 200);
2488 map.segment((1, 2)).unwrap().color(color).comet();
2489
2490 let expected_fill = scale_alpha(color, 0.7);
2496 assert_eq!(
2497 expected_fill.a(),
2498 140,
2499 "sanity-check the hand-derived alpha"
2500 );
2501
2502 let ctx = Context::default();
2503 let screen_rect = Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0));
2504 let mut out = ctx.run_ui(
2505 RawInput {
2506 screen_rect: Some(screen_rect),
2507 ..RawInput::default()
2508 },
2509 |ui| {
2510 ui.add(&mut map);
2511 },
2512 );
2513 out.textures_delta.clear();
2517
2518 let comet_circle = out
2519 .shapes
2520 .iter()
2521 .find_map(|cs| match &cs.shape {
2522 Shape::Circle(c) => Some(*c),
2523 _ => None,
2524 })
2525 .expect("comet must paint a filled circle");
2526
2527 assert_eq!(
2528 comet_circle.fill, expected_fill,
2529 "the comet's fill must be exactly scale_alpha(color, effect_fade) -- the line's \
2530 zoom fade with the documented head start applied"
2531 );
2532 }
2533
2534 #[test]
2535 fn color_modifier_reaches_both_segment_families() {
2536 let mut map = map_with_segments();
2537 map.segment((1, 2))
2538 .unwrap()
2539 .color(Color32::RED)
2540 .flash(Instant::now());
2541 map.segment((3, 4)).unwrap().color(Color32::BLUE).comet();
2542
2543 assert_eq!(
2544 map.segment_notifications.get(&(1, 2)).unwrap().color,
2545 Some(Color32::RED)
2546 );
2547 assert_eq!(
2548 map.segment_states.get(&(3, 4)).unwrap().color,
2549 Some(Color32::BLUE)
2550 );
2551 }
2552
2553 #[test]
2554 fn a_segment_can_carry_state_and_a_notification_at_once() {
2555 let mut map = map_with_segments();
2556 map.segment((1, 2)).unwrap().comet();
2557 map.segment((1, 2)).unwrap().flash(Instant::now());
2558
2559 assert!(map.segment_states.contains_key(&(1, 2)));
2560 assert!(map.segment_notifications.contains_key(&(1, 2)));
2561 }
2562
2563 #[test]
2564 fn clear_removes_both_segment_families_for_that_id_only() {
2565 let mut map = map_with_segments();
2566 map.segment((1, 2)).unwrap().comet();
2567 map.segment((1, 2)).unwrap().flash(Instant::now());
2568 map.segment((3, 4)).unwrap().comet();
2569
2570 map.segment((1, 2)).unwrap().clear();
2571
2572 assert!(!map.segment_states.contains_key(&(1, 2)));
2573 assert!(!map.segment_notifications.contains_key(&(1, 2)));
2574 assert!(
2575 map.segment_states.contains_key(&(3, 4)),
2576 "segment (3, 4) must be untouched"
2577 );
2578 }
2579
2580 #[test]
2581 fn re_flashing_a_segment_restarts_it() {
2582 let mut map = map_with_segments();
2583 let t1 = Instant::now();
2584 map.segment((1, 2)).unwrap().flash(t1);
2585 let t2 = t1 + Duration::from_secs(1);
2586 map.segment((1, 2)).unwrap().flash(t2);
2587
2588 assert_eq!(map.segment_notifications.len(), 1);
2589 assert_eq!(map.segment_notifications.get(&(1, 2)).unwrap().started, t2);
2590 }
2591
2592 #[test]
2593 fn update_marker_inserts_and_updates() {
2594 let mut map = Map::new();
2595 map.update_marker(1, 100);
2596 assert_eq!(map.markers.get(&1), Some(&100));
2597 map.update_marker(1, 200);
2598 assert_eq!(map.markers.get(&1), Some(&200));
2599 assert_eq!(map.markers.len(), 1);
2600 }
2601
2602 #[test]
2605 fn allocate_at_least_sets_min_size() {
2606 let mut map = Map::new();
2607 map.allocate_at_least(Some(100.0), None);
2608 assert_eq!(map.min_size, (Some(100.0), None));
2609 }
2610
2611 #[test]
2612 fn allocate_at_most_sets_max_size() {
2613 let mut map = Map::new();
2614 map.allocate_at_most(None, Some(200.0));
2615 assert_eq!(map.max_size, (None, Some(200.0)));
2616 }
2617
2618 #[test]
2621 fn adjust_bounds_scales_with_zoom() {
2622 let mut map = Map::new();
2623 map.reference.min = RawPoint::new(-10.0, -20.0);
2624 map.reference.max = RawPoint::new(10.0, 20.0);
2625 map.reference.pos = RawPoint::new(5.0, 5.0);
2626 map.reference.dist = 100.0;
2627 map.set_zoom(2.0);
2628 map.adjust_bounds();
2629
2630 assert_eq!(map.current.max.components, [20.0, 40.0]);
2631 assert_eq!(map.current.min.components, [-20.0, -40.0]);
2632 assert_eq!(map.current.pos.components, [10.0, 10.0]);
2633 assert_eq!(map.current.dist, 50.0);
2634 }
2635
2636 #[test]
2639 fn set_theme_refreshes_both_style_slots_immediately() {
2640 let mut map = Map::new();
2645 map.set_theme(Rc::new(Theme::ArticCyan));
2646
2647 let light = Theme::ArticCyan.colors(ColorMode::Light);
2648 let dark = Theme::ArticCyan.colors(ColorMode::Dark);
2649
2650 let light_style = &map.settings.styles[0];
2651 assert_eq!(light_style.fill_color, light.node);
2652 assert_eq!(light_style.text_color, light.text);
2653 assert_eq!(light_style.alert_color, light.alert);
2654 assert_eq!(light_style.line.unwrap().color, light.segment);
2655
2656 let dark_style = &map.settings.styles[1];
2657 assert_eq!(dark_style.fill_color, dark.node);
2658 assert_eq!(dark_style.text_color, dark.text);
2659 assert_eq!(dark_style.alert_color, dark.alert);
2660 assert_eq!(dark_style.line.unwrap().color, dark.segment);
2661 }
2662
2663 #[test]
2664 fn a_custom_map_theme_reaches_the_painted_node() {
2665 use crate::map::theme::ThemeColors;
2669 use egui::{Context, RawInput, Shape};
2670
2671 struct FixedPalette;
2672 impl MapTheme for FixedPalette {
2673 fn colors(&self, _mode: ColorMode) -> ThemeColors {
2674 ThemeColors {
2675 node: Color32::from_rgb(1, 2, 3),
2676 segment: Color32::from_rgb(4, 5, 6),
2677 selected: Color32::from_rgb(7, 8, 9),
2678 alert: Color32::from_rgb(10, 11, 12),
2679 text: Color32::from_rgb(13, 14, 15),
2680 }
2681 }
2682 }
2683
2684 let mut map = Map::new();
2685 map.set_theme(Rc::new(FixedPalette));
2686 map.add_points(vec![MapPoint::new(1, [0.0, 0.0])]);
2687
2688 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 200.0));
2689 let ctx = Context::default();
2690 let mut output = ctx.run_ui(
2691 RawInput {
2692 screen_rect: Some(screen),
2693 ..RawInput::default()
2694 },
2695 |ui| {
2696 ui.add(&mut map);
2697 },
2698 );
2699 output.textures_delta.clear();
2701
2702 let fill = output
2703 .shapes
2704 .iter()
2705 .find_map(|cs| match &cs.shape {
2706 Shape::Circle(circle) => Some(circle.fill),
2707 _ => None,
2708 })
2709 .expect("the node must be painted");
2710
2711 assert_eq!(
2712 fill,
2713 Color32::from_rgb(1, 2, 3),
2714 "the node fill must come from the installed MapTheme, in either color mode"
2715 );
2716 }
2717}