1use crate::map::animation::Animation;
72use crate::map::objects::{
73 ContextMenuManager, MapBounds, MapLabel, MapPoint, MapSegment, MapSettings, MapStyle,
74 NodeAnimation, RawLine, RawPoint, SteadyAnimation, TextSettings, VisibilitySetting,
75};
76use egui::{widgets::*, *};
77use kdtree::KdTree;
78use kdtree::distance::squared_euclidean;
79use std::collections::HashMap;
80use std::rc::Rc;
81use std::time::Instant;
82
83use self::objects::NodeTemplate;
84
85pub mod animation;
86pub mod objects;
87
88#[derive(Clone)]
126pub struct Map {
127 zoom: f32,
128 previous_zoom: f32,
129 points: Option<HashMap<usize, MapPoint>>,
130 segments: Option<rstar::RTree<MapSegment>>,
131 labels: Vec<MapLabel>,
132 tree: Option<KdTree<f32, usize, [f32; 2]>>,
133 visible_points: Vec<isize>,
134 map_area: Rect,
135 reference: MapBounds,
136 current: MapBounds,
137 current_index: usize,
138 notifications: HashMap<usize, Notification>,
139 node_states: HashMap<usize, NodeState>,
140 min_size: (Option<f32>, Option<f32>),
141 max_size: (Option<f32>, Option<f32>),
142 pub settings: MapSettings,
145 menu_manager: Option<Rc<dyn ContextMenuManager>>,
146 node_template: Option<Rc<dyn NodeTemplate>>,
147 markers: HashMap<usize, usize>,
148}
149
150#[derive(Clone, Copy, Debug)]
152struct Notification {
153 started: Instant,
154 animation: NodeAnimation,
155 color: Option<Color32>,
157}
158
159#[derive(Clone, Copy, Debug)]
161struct NodeState {
162 animation: SteadyAnimation,
163 color: Option<Color32>,
165}
166
167pub struct NodeHandle<'a> {
187 map: &'a mut Map,
188 id: usize,
189 color: Option<Color32>,
190}
191
192impl NodeHandle<'_> {
193 pub fn color(mut self, color: Color32) -> Self {
197 self.color = Some(color);
198 self
199 }
200
201 fn notify_with(self, animation: NodeAnimation, at: Instant) {
202 self.map.notifications.insert(
203 self.id,
204 Notification {
205 started: at,
206 animation,
207 color: self.color,
208 },
209 );
210 }
211
212 fn set_state(self, animation: SteadyAnimation) {
213 self.map.node_states.insert(
214 self.id,
215 NodeState {
216 animation,
217 color: self.color,
218 },
219 );
220 }
221
222 pub fn pulse(self, at: Instant) {
224 self.notify_with(NodeAnimation::Pulse, at);
225 }
226
227 pub fn ripple(self, at: Instant) {
229 self.notify_with(NodeAnimation::Ripple, at);
230 }
231
232 pub fn countdown(self, at: Instant) {
234 self.notify_with(NodeAnimation::CountdownArc, at);
235 }
236
237 pub fn scale_in(self, at: Instant) {
239 self.notify_with(NodeAnimation::ScaleIn, at);
240 }
241
242 pub fn crosshair(self, at: Instant) {
244 self.notify_with(NodeAnimation::Crosshair, at);
245 }
246
247 pub fn halo(self) {
249 self.set_state(SteadyAnimation::Halo);
250 }
251
252 pub fn blink(self) {
254 self.set_state(SteadyAnimation::Blink);
255 }
256
257 pub fn orbit(self) {
259 self.set_state(SteadyAnimation::Orbit);
260 }
261
262 pub fn clear(self) {
264 self.map.notifications.remove(&self.id);
265 self.map.node_states.remove(&self.id);
266 }
267}
268
269impl Default for Map {
270 fn default() -> Self {
272 Map::new()
273 }
274}
275
276impl Widget for &mut Map {
277 fn ui(self, ui: &mut egui::Ui) -> Response {
280 let rect = self.calculate_widget_dimensions(ui);
281
282 self.reference.dist = rect.distance();
284
285 self.assign_visual_style(ui);
286
287 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
288
289 let frame_margin = canvas.total_margin().sum();
299 let painter_size = (self.map_area.size() - frame_margin).max(Vec2::ZERO);
300
301 let inner_response = canvas.show(ui, |ui| {
302 let _span = tracing::info_span!("paint_map").entered();
303
304 if ui.is_rect_visible(self.map_area) {
305 let (resp, paint) =
306 ui.allocate_painter(painter_size, egui::Sense::click_and_drag());
307 let vec = resp.drag_delta();
308 if vec.length() != 0.0 {
309 let _span = tracing::info_span!("calculating_points_in_visible_area").entered();
310
311 let coords = RawPoint::from(vec.to_pos2());
312 let new_pos = self.reference.pos - (coords / self.zoom);
313 self.set_pos(new_pos.into());
314 }
315 if self.zoom < self.settings.line_visible_zoom {
316 let mut text_settings = TextSettings {
318 size: self.settings.label_text_size,
322 anchor: Align2::CENTER_CENTER,
323 family: FontFamily::Proportional,
324 text: String::new(),
325 position: RawPoint::default(),
326 text_color: ui.visuals().text_color(),
327 };
328 for label in &self.labels {
329 text_settings.text.clone_from(&label.text);
330 text_settings.position = RawPoint::from(label.center);
331 self.paint_label(&paint, &text_settings);
332 }
333 }
334
335 let rect_midpoint = RawPoint::from(resp.rect.center());
341 let min_point = self.current.pos - rect_midpoint;
342 let vec_points = &self.visible_points;
343 let hashm = &self.points;
344
345 let now = Instant::now();
348 self.notifications
349 .retain(|_, n| now.duration_since(n.started).as_secs_f32() < 10.0);
350
351 self.paint_map_lines(&paint, &min_point);
352
353 if let Ok(nodes_to_remove) =
354 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
355 {
356 for node in nodes_to_remove {
357 self.notifications.remove(&node);
358 }
359 }
360
361 for marker in &self.markers {
362 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
363 let adjusted_point = RawPoint::from(point.coords) * self.zoom - min_point;
364 if let Some(template) = &self.node_template {
365 template.marker_ui(ui, adjusted_point.into(), self.zoom);
366 } else {
367 let color = if ui.visuals().dark_mode {
368 Color32::LIGHT_GREEN
369 } else {
370 Color32::GREEN
371 };
372 let time = ui.input(|i| i.time) as f32;
376 let effect = match self.settings.marker_animation {
377 SteadyAnimation::Blink => Animation::blink,
378 SteadyAnimation::Halo => Animation::halo,
379 SteadyAnimation::Orbit => Animation::orbit,
380 };
381 effect(ui.painter(), adjusted_point.into(), self.zoom, time, color);
382 ui.ctx().request_repaint();
384 }
385 }
386 }
387
388 self.paint_sub_components(ui, self.map_area);
389
390 self.capture_mouse_events(ui, &resp);
391
392 if self.zoom != self.previous_zoom {
393 let _span = tracing::info_span!("calculating viewport with zoom").entered();
394 self.adjust_bounds();
395 self.calculate_visible_points();
396 self.previous_zoom = self.zoom;
397 }
398
399 if let Some(menu_mon) = &mut self.menu_manager {
400 resp.context_menu(|ui| {
401 menu_mon.ui(ui);
402 });
403 }
404
405 #[cfg(feature = "debug_overlay")]
406 self.print_debug_info(ui, &resp);
407 }
408 });
409 inner_response.response
414 }
415}
416
417impl Map {
418 pub fn new() -> Self {
423 let settings = MapSettings::default();
424 Self {
425 zoom: 1.0,
426 previous_zoom: 1.0,
427 map_area: Rect::NOTHING,
428 tree: None,
429 points: None,
430 labels: Vec::new(),
431 visible_points: Vec::new(),
432 current: MapBounds::default(),
433 reference: MapBounds::default(),
434 settings,
435 min_size: (None, None),
436 max_size: (None, None),
437 current_index: 0,
438 notifications: HashMap::new(),
439 node_states: HashMap::new(),
440 menu_manager: None,
441 node_template: None,
442 markers: HashMap::new(),
443 segments: None,
444 }
445 }
446
447 fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
448 let available = ui.available_rect_before_wrap();
449 let mut size = available.size();
450 if let Some(max_width) = self.max_size.0 {
451 size.x = size.x.min(max_width);
452 }
453 if let Some(max_height) = self.max_size.1 {
454 size.y = size.y.min(max_height);
455 }
456 if let Some(min_width) = self.min_size.0 {
457 size.x = size.x.max(min_width);
458 }
459 if let Some(min_height) = self.min_size.1 {
460 size.y = size.y.max(min_height);
461 }
462 self.map_area = Rect::from_min_size(available.min, size);
463 RawLine::new(
464 RawPoint::from(self.map_area.left_top()),
465 RawPoint::from(self.map_area.right_bottom()),
466 )
467 }
468
469 fn calculate_visible_points(&mut self) {
470 let _span = tracing::info_span!("calculate_visible_points").entered();
471 if self.current.dist > 0.0
472 && self.current.dist < f32::INFINITY
473 && let Some(tree) = &self.tree
474 {
475 let center = self.current.pos / self.zoom;
476 let radius = self.current.dist.powi(2);
477 let point: [f32; 2] = center.into();
478 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
479 self.visible_points.clear();
480 for point in vis_pos {
481 self.visible_points.push(point.1.cast_signed());
482 }
483 }
484 }
485
486 pub fn add_points(&mut self, points: Vec<MapPoint>) {
514 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
515 let mut hash_map = HashMap::new();
516 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
517 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
518 for entry in points {
519 for i in 0..min.components.len() {
520 if entry.coords[i] < min.components[i] {
521 min.components[i] = entry.coords[i];
522 }
523 if entry.coords[i] > max.components[i] {
524 max.components[i] = entry.coords[i];
525 }
526 }
527 let _result = tree.add(entry.coords, entry.get_id());
528 hash_map.insert(entry.get_id(), entry);
529 }
530 self.reference.min = min;
532 self.reference.max = max;
533 self.points = Some(hash_map);
534 self.tree = Some(tree);
535 self.reference.pos = RawLine::new(min, max).midpoint();
536 if self.map_area.area() == 0.0 {
540 self.reference.dist = 3000.00;
541 } else {
542 let rect = RawLine::new(
543 RawPoint::from(self.map_area.left_top()),
544 RawPoint::from(self.map_area.right_bottom()),
545 );
546 self.reference.dist = rect.distance();
547 }
548 self.current = self.reference.clone();
549 self.calculate_visible_points();
550 }
551
552 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
582 let _span = tracing::info_span!("add_hashmap_points").entered();
583 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
584 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
585 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
586
587 for entry in hash_map.iter() {
588 for i in 0..min.components.len() {
589 if entry.1.coords[i] < min.components[i] {
590 min.components[i] = entry.1.coords[i];
591 }
592 if entry.1.coords[i] > max.components[i] {
593 max.components[i] = entry.1.coords[i];
594 }
595 }
596 let _result = tree.add(entry.1.coords, *entry.0);
597 }
598
599 self.reference.min = min;
601 self.reference.max = max;
602 self.points = Some(hash_map);
603 self.tree = Some(tree);
604 self.reference.pos = RawLine::new(min, max).midpoint();
605 if self.map_area.area() == 0.0 {
609 self.reference.dist = 3000.00;
610 } else {
611 let rect = RawLine::new(
612 RawPoint::from(self.map_area.left_top()),
613 RawPoint::from(self.map_area.right_bottom()),
614 );
615 self.reference.dist = rect.distance();
616 }
617 self.current = self.reference.clone();
618 self.calculate_visible_points();
619 }
620
621 pub fn set_pos_from_nodeid(&mut self, node_id: usize) -> bool {
649 let _span = tracing::info_span!("set_pos_from_nodeid").entered();
650 if let Some(hash_map) = &self.points
651 && let Some(map_point) = hash_map.get(&node_id)
652 {
653 self.reference.pos = RawPoint::from(map_point.coords);
654 self.adjust_bounds();
655 self.calculate_visible_points();
656 true
657 } else {
658 tracing::warn!(
659 node_id,
660 loaded_nodes = self.points.as_ref().map_or(0, |p| p.len()),
661 "set_pos_from_nodeid: unknown node id, the view was left unchanged"
662 );
663 false
664 }
665 }
666
667 pub fn set_pos(&mut self, position: [f32; 2]) {
669 let _span = tracing::info_span!("set_pos").entered();
670 let point = RawPoint::from(position);
671 self.reference.pos = point;
672 self.adjust_bounds();
673 self.calculate_visible_points();
674 }
675
676 pub fn get_pos(&self) -> [f32; 2] {
678 let _span = tracing::info_span!("get_pos").entered();
679 self.reference.pos.into()
680 }
681
682 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
687 let _span = tracing::info_span!("add_labels").entered();
688 self.labels = labels;
689 }
690
691 pub fn add_lines(&mut self, segments: Vec<MapSegment>) {
703 let _span = tracing::info_span!("add_lines").entered();
704 self.segments = Some(rstar::RTree::bulk_load(segments));
709 }
710
711 pub fn add_hashmap_lines(&mut self, segments: HashMap<(usize, usize), MapSegment>) {
720 let _span = tracing::info_span!("add_hashmap_lines").entered();
721 let segments: Vec<MapSegment> = segments.into_values().collect();
722 self.segments = Some(rstar::RTree::bulk_load(segments));
723 }
724
725 fn adjust_bounds(&mut self) {
726 let _span = tracing::info_span!("adjust_bounds").entered();
727 self.current.max = self.reference.max * self.zoom;
728 self.current.min = self.reference.min * self.zoom;
729 self.current.dist = self.reference.dist / self.zoom;
730 self.current.pos = self.reference.pos * self.zoom;
731 }
732
733 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
734 let _span = tracing::info_span!("capture_mouse_events").entered();
735 if ui.rect_contains_pointer(self.map_area) {
737 ui.input(|x| {
738 let _span = tracing::info_span!("capture_mouse_events_input").entered();
739
740 if !x.events.is_empty() {
741 for event in &x.events {
742 match event {
743 Event::MouseWheel {
744 unit: _,
745 delta,
746 modifiers,
747 phase: _,
748 } => {
749 #[cfg(target_os = "macos")]
750 let zoom_modifier = if modifiers.mac_cmd {
751 delta.y / 80.00
752 } else {
753 delta.y / 400.00
754 };
755
756 #[cfg(not(target_os = "macos"))]
757 let zoom_modifier = if modifiers.ctrl {
758 delta.y / 8.00
759 } else {
760 delta.y / 40.00
761 };
762
763 let mut pre_zoom = self.zoom + zoom_modifier;
764 if pre_zoom > self.settings.max_zoom {
765 pre_zoom = self.settings.max_zoom;
766 }
767 if pre_zoom < self.settings.min_zoom {
768 pre_zoom = self.settings.min_zoom;
769 }
770 self.zoom = pre_zoom;
771 }
772 _ => {
773 continue;
774 }
775 };
776 }
777 }
778 });
779 }
780 }
781
782 pub fn set_zoom(&mut self, value: f32) {
787 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
788 self.zoom = value;
789 }
790 }
791
792 pub fn get_zoom(&mut self) -> f32 {
794 self.zoom
795 }
796
797 fn current_style(&self) -> &MapStyle {
800 self.settings
801 .styles
802 .get(self.current_index)
803 .or(self.settings.styles.first())
804 .expect("MapSettings::styles must not be empty")
805 }
806
807 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
808 let style_index = ui_obj.visuals().dark_mode as usize;
809
810 if self.current_index != style_index {
811 let _span = tracing::info_span!("asign_visual_style").entered();
812
813 self.current_index = style_index;
814 let map_style = self.settings.styles.get_mut(style_index).unwrap();
815 let visuals = &ui_obj.style().visuals;
816 map_style.background_color = visuals.extreme_bg_color;
817 map_style.border = Some(visuals.window_stroke);
818 }
819 }
820
821 #[cfg(feature = "debug_overlay")]
830 fn print_debug_info(&mut self, ui: &mut Ui, resp: &Response) {
831 let _span = tracing::info_span!("printing debug data").entered();
832
833 let p = |v: f32| format!("{v:.2}");
834 let mut rows: Vec<(String, Color32)> = vec![
835 (
836 format!(
837 "MIN {}, {}",
838 p(self.current.min.components[0]),
839 p(self.current.min.components[1])
840 ),
841 Color32::LIGHT_GREEN,
842 ),
843 (
844 format!(
845 "MAX {}, {}",
846 p(self.current.max.components[0]),
847 p(self.current.max.components[1])
848 ),
849 Color32::LIGHT_GREEN,
850 ),
851 (
852 format!(
853 "CUR {}, {}",
854 p(self.current.pos.components[0]),
855 p(self.current.pos.components[1])
856 ),
857 Color32::LIGHT_GREEN,
858 ),
859 (
860 format!("DST {}", p(self.current.dist)),
861 Color32::LIGHT_GREEN,
862 ),
863 (format!("ZOM {}", self.zoom), Color32::GREEN),
864 (
865 format!(
866 "REC {}, {} .. {}, {}",
867 p(self.map_area.left_top().x),
868 p(self.map_area.left_top().y),
869 p(self.map_area.right_bottom().x),
870 p(self.map_area.right_bottom().y)
871 ),
872 Color32::LIGHT_GREEN,
873 ),
874 ];
875 if let Some(points) = &self.points {
876 rows.push((format!("NUM {}", points.len()), Color32::LIGHT_GREEN));
877 }
878 if !self.visible_points.is_empty() {
879 rows.push((
880 format!("VIS {}", self.visible_points.len()),
881 Color32::LIGHT_GREEN,
882 ));
883 }
884 if let Some(pointer_pos) = resp.hover_pos() {
885 rows.push((
886 format!("HVR {}, {}", p(pointer_pos.x), p(pointer_pos.y)),
887 Color32::LIGHT_BLUE,
888 ));
889 }
890 let drag = resp.drag_delta();
891 if drag.length() != 0.0 {
892 rows.push((format!("DRG {}, {}", p(drag.x), p(drag.y)), Color32::GOLD));
893 }
894
895 let overlay_rect = Rect::from_min_max(
905 self.map_area.left_top() + Vec2::new(6.0, 6.0),
906 self.map_area.right_bottom(),
907 );
908 let mut overlay_ui = ui.new_child(
909 UiBuilder::new()
910 .max_rect(overlay_rect)
911 .layout(Layout::top_down(Align::Min)),
912 );
913 CollapsingHeader::new(RichText::new("dbg").monospace().small().weak())
916 .id_salt("egui_map_debug_overlay")
917 .default_open(false)
918 .show_background(false)
919 .show(&mut overlay_ui, |ui| {
920 for (text, color) in rows {
921 ui.label(RichText::new(text).monospace().small().color(color));
922 }
923 });
924 }
925
926 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
927 let _span = tracing::info_span!("map_ui_paint_sub_components").entered();
928 let zoom_slider = egui::Slider::new(
929 &mut self.zoom,
930 self.settings.min_zoom..=self.settings.max_zoom,
931 )
932 .show_value(false)
933 .orientation(SliderOrientation::Vertical);
934 let mut pos1 = rect.right_top();
935 let mut pos2 = rect.right_top();
936 pos1.x -= 80.0;
937 pos1.y += 120.0;
938 pos2.x -= 60.0;
939 pos2.y += 240.0;
940
941 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
942 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
943 ui_obj.scope_builder(ui_builder, |ui_obj| {
944 ui_obj.add(zoom_slider);
945 });
946 }
947
948 fn paint_map_points(
949 &self,
950 vec_points: &Vec<isize>,
951 hashm: &Option<HashMap<usize, MapPoint>>,
952 paint: &Painter,
953 ui_obj: &mut Ui,
954 min_point: &RawPoint,
955 resp: &Response,
956 ) -> Result<Vec<usize>, ()> {
957 let mut nearest_id = None;
958 let mut nodes_to_remove = Vec::new();
959 let mut shape_vec = vec![];
960
961 if hashm.is_none() {
962 return Err(());
963 }
964 if vec_points.is_empty() {
965 return Err(());
966 }
967 if self.settings.node_text_visibility == VisibilitySetting::Hover
969 && resp.hovered()
970 && let Some(point) = resp.hover_pos()
971 {
972 let raw_point = RawPoint::from(point);
973 let hovered_map_point = (*min_point + raw_point) / self.zoom;
974 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
975 &hovered_map_point.components,
976 1,
977 &squared_euclidean,
978 ) {
979 nearest_id = Some(nearest_node.first().unwrap().1);
980 }
981 }
982 let mut text_settings = TextSettings {
984 size: self.settings.node_text_size,
988 anchor: Align2::LEFT_BOTTOM,
989 family: FontFamily::Proportional,
990 text: String::new(),
991 position: RawPoint::default(),
992 text_color: ui_obj.visuals().text_color(),
993 };
994
995 for temp_point in vec_points {
997 let parsed_point = temp_point.cast_unsigned();
998 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
999 let _span = tracing::info_span!("painting_points_m").entered();
1000 let viewport_point = RawPoint::from(system.coords) * self.zoom - min_point;
1001 if let Some(node_template) = &self.node_template {
1002 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
1003 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
1004 }
1005 } else if self.zoom > self.settings.label_visible_zoom
1006 && self.settings.node_text_visibility == VisibilitySetting::Always
1007 || (self.settings.node_text_visibility == VisibilitySetting::Hover
1008 && nearest_id.unwrap_or(&0usize) == &system.get_id())
1009 {
1010 let mut viewport_text = viewport_point;
1011 viewport_text.components[0] += 3.0 * self.zoom;
1012 viewport_text.components[1] -= 3.0 * self.zoom;
1013 text_settings.position = viewport_text;
1014 text_settings.text = system.get_name();
1015 self.paint_label(paint, &text_settings);
1016 }
1017
1018 let system_id = system.get_id();
1019
1020 if let Some(state) = self.node_states.get(&system_id) {
1023 let color = state.color.unwrap_or(self.current_style().alert_color);
1024 if let Some(template) = &self.node_template {
1025 template.marker_ui(ui_obj, viewport_point.into(), self.zoom);
1030 } else {
1031 let effect = match state.animation {
1032 SteadyAnimation::Blink => Animation::blink,
1033 SteadyAnimation::Halo => Animation::halo,
1034 SteadyAnimation::Orbit => Animation::orbit,
1035 };
1036 let time = ui_obj.input(|i| i.time) as f32;
1039 effect(paint, viewport_point.into(), self.zoom, time, color);
1040 }
1041 ui_obj.ctx().request_repaint();
1043 }
1044
1045 if let Some(notification) = self.notifications.get(&system_id) {
1046 let color = notification
1047 .color
1048 .unwrap_or(self.current_style().alert_color);
1049 if let Some(template) = &self.node_template {
1050 template.notification_ui(
1051 ui_obj,
1052 viewport_point.into(),
1053 self.zoom,
1054 notification.started,
1055 color,
1056 );
1057 } else {
1058 let effect = match notification.animation {
1059 NodeAnimation::Pulse => Animation::pulse,
1060 NodeAnimation::Ripple => Animation::ripple,
1061 NodeAnimation::CountdownArc => Animation::countdown_arc,
1062 NodeAnimation::ScaleIn => Animation::scale_in,
1063 NodeAnimation::Crosshair => Animation::crosshair,
1064 };
1065 if effect(
1066 paint,
1067 viewport_point.into(),
1068 self.zoom,
1069 notification.started,
1070 color,
1071 ) {
1072 ui_obj.ctx().request_repaint();
1073 } else {
1074 nodes_to_remove.push(system_id);
1075 }
1076 }
1077 }
1078 if let Some(node_template) = &self.node_template {
1079 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
1080 } else {
1081 shape_vec.push(Shape::circle_filled(
1082 viewport_point.into(),
1083 4.00 * self.zoom,
1084 system.color.unwrap_or(self.current_style().fill_color),
1085 ));
1086 }
1087 }
1088 }
1089 paint.extend(shape_vec);
1090 Ok(nodes_to_remove)
1091 }
1092
1093 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
1094 let _span = tracing::info_span!("paint_map_lines").entered();
1095
1096 if self.zoom > self.settings.line_visible_zoom
1098 && let Some(mut stroke) = self.current_style().line
1099 && let Some(segments) = &self.segments
1100 {
1101 let mut shape_vec = vec![];
1102 let transparency_range = self.zoom - self.settings.line_visible_zoom;
1103 if (0.00..0.80).contains(&transparency_range) {
1104 let mut tup_stroke = stroke.color.to_tuple();
1105 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
1106 tup_stroke.3 = (255.0 * transparency).round() as u8;
1107 let color = Color32::from_rgba_unmultiplied(
1108 tup_stroke.0,
1109 tup_stroke.1,
1110 tup_stroke.2,
1111 tup_stroke.3,
1112 );
1113 stroke = Stroke::new(stroke.width, color);
1114 }
1115 let center = self.current.pos / self.zoom;
1119 let padding = stroke.width / self.zoom;
1120 let half = RawPoint::new(
1121 self.map_area.width() / 2.0 / self.zoom + padding,
1122 self.map_area.height() / 2.0 / self.zoom + padding,
1123 );
1124 let query = rstar::AABB::from_corners((center - half).into(), (center + half).into());
1125 for segment in segments.locate_in_envelope_intersecting(query) {
1126 let raw_line = segment.raw_line();
1127 let pos_a = raw_line.points[0] * self.zoom - min_point;
1128 let pos_b = raw_line.points[1] * self.zoom - min_point;
1129 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
1130 }
1131 painter.extend(shape_vec);
1132 }
1133 }
1134
1135 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
1136 let _span = tracing::info_span!("paint_label").entered();
1137 paint.text(
1138 text_settings.position.into(),
1139 text_settings.anchor,
1140 text_settings.text.clone(),
1141 FontId::new(text_settings.size, text_settings.family.clone()),
1142 text_settings.text_color,
1143 );
1144 }
1145
1146 #[deprecated(
1169 since = "0.4.0",
1170 note = "use `map.node(id)` and pick an effect, e.g. `if let Some(n) = map.node(id) { n.pulse(time) }`"
1171 )]
1172 pub fn notify(&mut self, id_node: usize, time: Instant) {
1173 let _span = tracing::info_span!("notify").entered();
1174 self.notifications.insert(
1175 id_node,
1176 Notification {
1177 started: time,
1178 animation: NodeAnimation::Pulse,
1179 color: None,
1180 },
1181 );
1182 }
1183
1184 pub fn node(&mut self, id: usize) -> Option<NodeHandle<'_>> {
1213 if !self
1214 .points
1215 .as_ref()
1216 .is_some_and(|points| points.contains_key(&id))
1217 {
1218 return None;
1219 }
1220 Some(NodeHandle {
1221 map: self,
1222 id,
1223 color: None,
1224 })
1225 }
1226
1227 pub fn line_at(&self, point: [f32; 2], tolerance: f32) -> Option<(usize, usize)> {
1241 let _span = tracing::info_span!("line_at").entered();
1242 let segments = self.segments.as_ref()?;
1243 let tolerance = tolerance.max(0.0);
1244
1245 let center = RawPoint::from(point);
1246 let padding = RawPoint::new(tolerance, tolerance);
1247 let query = rstar::AABB::from_corners((center - padding).into(), (center + padding).into());
1248
1249 let mut closest: Option<(f32, (usize, usize))> = None;
1250 for segment in segments.locate_in_envelope_intersecting(query) {
1251 let distance = segment.raw_line().distance_to_point(center);
1252 if distance <= tolerance && closest.as_ref().is_none_or(|(best, _)| distance < *best) {
1253 closest = Some((distance, segment.id));
1254 }
1255 }
1256 closest.map(|(_, id)| id)
1257 }
1258
1259 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
1262 self.menu_manager = Some(manager);
1263 }
1264
1265 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
1273 self.node_template = Some(template);
1274 }
1275
1276 pub fn update_marker(&mut self, id: usize, node_id: usize) {
1281 self.markers
1282 .entry(id)
1283 .and_modify(|value| *value = node_id)
1284 .or_insert(node_id);
1285 }
1286
1287 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
1290 self.min_size = (width, height);
1291 }
1292
1293 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
1296 self.max_size = (width, height);
1297 }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use super::*;
1303 use std::time::Duration;
1304
1305 fn sample_points() -> Vec<MapPoint> {
1306 let mut map = Vec::new();
1307 map.push(MapPoint::new(1, [0.0, 0.0]));
1308 map.push(MapPoint::new(2, [10.0, 10.0]));
1309 map.push(MapPoint::new(3, [-10.0, -10.0]));
1310 map
1311 }
1312
1313 #[test]
1316 fn map_new_initial_state() {
1317 let map = Map::new();
1318 assert_eq!(map.zoom, 1.0);
1319 assert_eq!(map.previous_zoom, 1.0);
1320 assert!(map.points.is_none());
1321 assert!(map.segments.is_none());
1322 assert!(map.tree.is_none());
1323 assert!(map.labels.is_empty());
1324 assert!(map.visible_points.is_empty());
1325 assert!(map.markers.is_empty());
1326 assert!(map.notifications.is_empty());
1327 assert!(map.node_states.is_empty());
1328 assert_eq!(map.min_size, (None, None));
1329 assert_eq!(map.max_size, (None, None));
1330 assert_eq!(map.current_index, 0);
1331 }
1332
1333 #[test]
1334 fn map_default_equals_new() {
1335 let map = Map::default();
1336 assert_eq!(map.zoom, 1.0);
1337 assert!(map.points.is_none());
1338 }
1339
1340 #[test]
1343 fn set_zoom_within_range() {
1344 let mut map = Map::new();
1345 map.set_zoom(1.5);
1346 assert_eq!(map.get_zoom(), 1.5);
1347 }
1348
1349 #[test]
1350 fn set_zoom_at_exact_limits() {
1351 let mut map = Map::new();
1352 map.set_zoom(map.settings.min_zoom);
1353 assert_eq!(map.get_zoom(), 0.1);
1354 map.set_zoom(map.settings.max_zoom);
1355 assert_eq!(map.get_zoom(), 2.0);
1356 }
1357
1358 #[test]
1359 fn set_zoom_out_of_range_is_ignored() {
1360 let mut map = Map::new();
1361 let initial = map.get_zoom();
1362 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
1364 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
1366 }
1367
1368 #[test]
1371 fn add_hashmap_points_computes_bounds() {
1372 let mut map = Map::new();
1373 map.add_points(sample_points());
1374
1375 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
1376 assert_eq!(map.reference.max.components, [10.0, 10.0]);
1377 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1379 assert_eq!(map.reference.dist, 3000.0);
1381 assert_eq!(map.current.min.components, map.reference.min.components);
1383 assert_eq!(map.current.max.components, map.reference.max.components);
1384 assert_eq!(map.current.pos.components, map.reference.pos.components);
1385 assert_eq!(map.current.dist, map.reference.dist);
1386 assert!(map.points.is_some());
1387 assert!(map.tree.is_some());
1388 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1389 }
1390
1391 #[test]
1392 fn add_hashmap_points_populates_visible_points() {
1393 let mut map = Map::new();
1394 map.add_points(sample_points());
1395 assert_eq!(map.visible_points.len(), 3);
1397 }
1398
1399 fn render_line_segments(map: &mut Map) -> Vec<[egui::Pos2; 2]> {
1402 use egui::{Context, RawInput, Shape};
1403 let ctx = Context::default();
1404 let input = RawInput {
1405 screen_rect: Some(egui::Rect::from_min_size(
1406 egui::Pos2::ZERO,
1407 egui::vec2(500.0, 500.0),
1408 )),
1409 ..RawInput::default()
1410 };
1411 let output = ctx.run_ui(input, |ui| {
1412 ui.add(&mut *map);
1413 });
1414 output
1415 .shapes
1416 .iter()
1417 .filter_map(|cs| match cs.shape {
1418 Shape::LineSegment { points, .. } => Some(points),
1419 _ => None,
1420 })
1421 .collect()
1422 }
1423
1424 #[test]
1425 fn segment_crossing_viewport_is_painted_even_with_far_endpoints() {
1426 let mut map = Map::new();
1431 map.set_zoom(1.0);
1432 let mut lines = Vec::new();
1433 lines.push(MapSegment::new((1, 2), [-4000.0, -1.0], [4000.0, 1.0]));
1434 map.add_lines(lines);
1435 map.set_pos([0.0, 0.0]);
1436
1437 let segments = render_line_segments(&mut map);
1438 assert_eq!(segments.len(), 1);
1439 }
1440
1441 #[test]
1442 fn segment_outside_viewport_is_not_painted() {
1443 let mut map = Map::new();
1444 map.set_zoom(1.0);
1445 let mut lines = Vec::new();
1446 lines.push(MapSegment::new(
1447 (1, 2),
1448 [10_000.0, 10_000.0],
1449 [10_100.0, 10_100.0],
1450 ));
1451 map.add_lines(lines);
1452 map.set_pos([0.0, 0.0]);
1453
1454 assert!(render_line_segments(&mut map).is_empty());
1455 }
1456
1457 #[test]
1458 fn add_lines_builds_segment_tree() {
1459 let mut map = Map::new();
1460 map.add_points(sample_points());
1461 let mut lines = Vec::new();
1462 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1463 map.add_lines(lines);
1464
1465 let tree = map
1466 .segments
1467 .as_ref()
1468 .expect("add_lines must build the segment tree");
1469 assert_eq!(tree.size(), 1);
1470
1471 let hit_query = rstar::AABB::from_corners([-1.0, -1.0], [1.0, 1.0]);
1474 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1475 assert_eq!(hits.len(), 1);
1476 assert_eq!(hits[0].id, (1, 2));
1477
1478 let miss_query = rstar::AABB::from_corners([100.0, 100.0], [200.0, 200.0]);
1479 assert_eq!(tree.locate_in_envelope_intersecting(miss_query).count(), 0);
1480 }
1481
1482 #[test]
1483 fn map_check_line_is_painted_on_first_frame() {
1484 use egui::{Context, RawInput, Shape};
1485
1486 let mut map = Map::new();
1488 map.set_zoom(1.0);
1489
1490 let mut point_a = MapPoint::new(0, [0.0, 0.0]);
1491 point_a.connections.push((0, 1));
1492 let mut point_b = MapPoint::new(1, [50.0, 50.0]);
1493 point_b.connections.push((0, 1));
1494
1495 let mut lines = Vec::new();
1496 lines.push(MapSegment::new((0, 1), point_a.coords, point_b.coords));
1497
1498 let mut points = Vec::new();
1499 points.push(point_a);
1500 points.push(point_b);
1501 map.add_points(points);
1503 map.add_lines(lines);
1504
1505 map.set_pos([25.0, 25.0]);
1506
1507 let ctx = Context::default();
1509 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1510 let input = RawInput {
1511 screen_rect: Some(screen),
1512 ..RawInput::default()
1513 };
1514
1515 let output1 = ctx.run_ui(input.clone(), |ui| {
1516 ui.add(&mut map);
1517 });
1518
1519 let segments1: Vec<[egui::Pos2; 2]> = output1
1520 .shapes
1521 .iter()
1522 .filter_map(|cs| match cs.shape {
1523 Shape::LineSegment { points, .. } => Some(points),
1524 _ => None,
1525 })
1526 .collect();
1527
1528 assert!(
1529 !segments1.is_empty(),
1530 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
1531 );
1532
1533 let expected_a = egui::pos2(225.0, 225.0);
1536 let expected_b = egui::pos2(275.0, 275.0);
1537 let tolerance = 2.0;
1538 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
1539 let d_a1 = p1.distance(expected_a);
1540 let d_b1 = p2.distance(expected_b);
1541 let d_a2 = p2.distance(expected_a);
1542 let d_b2 = p1.distance(expected_b);
1543 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
1544 });
1545 assert!(
1546 found_on_frame1,
1547 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
1548 segments1
1549 );
1550
1551 let output2 = ctx.run_ui(input, |ui| {
1553 ui.add(&mut map);
1554 });
1555
1556 let segments2: Vec<[egui::Pos2; 2]> = output2
1557 .shapes
1558 .iter()
1559 .filter_map(|cs| match cs.shape {
1560 Shape::LineSegment { points, .. } => Some(points),
1561 _ => None,
1562 })
1563 .collect();
1564
1565 assert_eq!(
1566 segments1.len(),
1567 segments2.len(),
1568 "Frame 2: expected {} line segments (no duplication across frames), got {}",
1569 segments1.len(),
1570 segments2.len()
1571 );
1572 }
1573
1574 #[test]
1577 fn set_pos_and_get_pos_roundtrip() {
1578 let mut map = Map::new();
1579 map.set_pos([25.0, -35.0]);
1580 assert_eq!(map.get_pos(), [25.0, -35.0]);
1581 }
1582
1583 #[test]
1584 fn set_pos_from_nodeid_with_valid_id() {
1585 let mut map = Map::new();
1586 map.add_points(sample_points());
1587 assert!(map.set_pos_from_nodeid(2));
1588 assert_eq!(map.get_pos(), [10.0, 10.0]);
1589 }
1590
1591 #[test]
1598 fn centered_node_is_painted_at_the_middle_of_the_drawable_area() {
1599 use egui::{Context, RawInput, Shape};
1600
1601 for screen_size in [egui::vec2(500.0, 500.0), egui::vec2(800.0, 400.0)] {
1602 for zoom in [1.0, 2.0] {
1603 let mut map = Map::new();
1604 map.set_zoom(zoom);
1605 map.add_points(vec![MapPoint::new(7, [123.0, 456.0])]);
1606 map.set_pos_from_nodeid(7);
1607
1608 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size);
1609 let ctx = Context::default();
1610 let mut widget_rect = egui::Rect::NOTHING;
1611 let mut output = ctx.run_ui(
1612 RawInput {
1613 screen_rect: Some(screen),
1614 ..RawInput::default()
1615 },
1616 |ui| {
1617 widget_rect = ui.add(&mut map).rect;
1618 },
1619 );
1620
1621 assert!(
1623 screen.contains_rect(widget_rect),
1624 "{screen_size:?} zoom {zoom}: widget rect {widget_rect:?} overflows {screen:?}"
1625 );
1626
1627 let (node_center, drawable) = output
1630 .shapes
1631 .iter()
1632 .find_map(|cs| match &cs.shape {
1633 Shape::Circle(circle) => Some((circle.center, cs.clip_rect)),
1634 _ => None,
1635 })
1636 .expect("the node must be painted");
1637
1638 assert!(
1639 node_center.distance(drawable.center()) < 0.5,
1640 "{screen_size:?} zoom {zoom}: node painted at {node_center:?} but the \
1641 drawable area {drawable:?} is centred at {:?}",
1642 drawable.center()
1643 );
1644
1645 output.textures_delta.clear();
1646 }
1647 }
1648 }
1649
1650 #[test]
1651 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1652 let mut map = Map::new();
1653 map.add_points(sample_points());
1654 let before = map.reference.pos.components;
1655 assert!(!map.set_pos_from_nodeid(999));
1657 assert_eq!(map.reference.pos.components, before);
1658 }
1659
1660 #[test]
1661 fn set_pos_from_nodeid_without_points_does_nothing() {
1662 let mut map = Map::new();
1663 assert!(!map.set_pos_from_nodeid(1));
1664 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1665 }
1666
1667 #[test]
1670 fn add_labels_stores_labels() {
1671 let mut map = Map::new();
1672 let label = MapLabel {
1673 text: "Region".to_string(),
1674 center: Pos2::new(1.0, 2.0),
1675 };
1676 map.add_labels(vec![label]);
1677 assert_eq!(map.labels.len(), 1);
1678 assert_eq!(map.labels[0].text, "Region");
1679 }
1680
1681 #[test]
1682 fn add_lines_stores_lines() {
1683 let mut map = Map::new();
1684 let mut lines = Vec::new();
1685 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [1.0, 1.0]));
1686 map.add_lines(lines);
1687 let tree = map.segments.as_ref().unwrap();
1688 assert_eq!(tree.size(), 1);
1689 assert_eq!(
1690 tree.locate_in_envelope_intersecting(rstar::AABB::from_corners(
1691 [-1.0, -1.0],
1692 [2.0, 2.0],
1693 ))
1694 .next()
1695 .unwrap()
1696 .id,
1697 (1, 2)
1698 );
1699 }
1700
1701 #[test]
1704 fn line_at_returns_closest_line_within_tolerance() {
1705 let mut map = Map::new();
1706 map.add_points(sample_points());
1707 let mut lines = Vec::new();
1708 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]));
1709 lines.push(MapSegment::new((3, 4), [20.0, -5.0], [20.0, 5.0]));
1710 map.add_lines(lines);
1711
1712 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
1714 assert_eq!(hit, (1, 2));
1715
1716 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
1718 assert_eq!(hit, (3, 4));
1719 }
1720
1721 #[test]
1722 fn line_at_returns_none_beyond_tolerance() {
1723 let mut map = Map::new();
1724 map.add_points(sample_points());
1725 let mut lines = Vec::new();
1726 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1727 map.add_lines(lines);
1728
1729 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
1732 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
1733 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
1734 }
1735
1736 #[test]
1737 fn line_at_returns_none_without_lines() {
1738 let map = Map::new();
1739 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
1740 }
1741
1742 #[test]
1743 fn line_at_negative_tolerance_behaves_like_zero() {
1744 let mut map = Map::new();
1745 map.add_points(sample_points());
1746 let mut lines = Vec::new();
1747 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1748 map.add_lines(lines);
1749
1750 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
1752 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
1753 }
1754
1755 #[test]
1758 #[allow(deprecated)]
1759 fn deprecated_notify_still_records_a_pulse() {
1760 let mut map = Map::new();
1761 let t1 = Instant::now();
1762 map.notify(5, t1);
1763 let recorded = map.notifications.get(&5).expect("notify must record");
1764 assert_eq!(recorded.started, t1);
1765 assert_eq!(recorded.animation, NodeAnimation::Pulse);
1766 assert_eq!(recorded.color, None);
1767
1768 let t2 = t1 + Duration::from_secs(1);
1769 map.notify(5, t2);
1770 assert_eq!(map.notifications.get(&5).unwrap().started, t2);
1771 assert_eq!(map.notifications.len(), 1);
1772 }
1773
1774 fn map_with_nodes() -> Map {
1777 let mut map = Map::new();
1778 map.add_points(vec![
1779 MapPoint::new(1, [0.0, 0.0]),
1780 MapPoint::new(2, [10.0, 10.0]),
1781 ]);
1782 map
1783 }
1784
1785 #[test]
1786 fn node_returns_none_for_an_unknown_id() {
1787 let mut map = map_with_nodes();
1788 assert!(map.node(1).is_some());
1789 assert!(map.node(999).is_none());
1790 assert!(Map::new().node(1).is_none());
1792 }
1793
1794 #[test]
1795 fn each_event_effect_records_its_own_animation() {
1796 let now = Instant::now();
1797 for (apply, expected) in [
1798 (
1799 Box::new(|n: NodeHandle| n.pulse(now)) as Box<dyn FnOnce(NodeHandle)>,
1800 NodeAnimation::Pulse,
1801 ),
1802 (
1803 Box::new(|n: NodeHandle| n.ripple(now)),
1804 NodeAnimation::Ripple,
1805 ),
1806 (
1807 Box::new(|n: NodeHandle| n.countdown(now)),
1808 NodeAnimation::CountdownArc,
1809 ),
1810 (
1811 Box::new(|n: NodeHandle| n.scale_in(now)),
1812 NodeAnimation::ScaleIn,
1813 ),
1814 (
1815 Box::new(|n: NodeHandle| n.crosshair(now)),
1816 NodeAnimation::Crosshair,
1817 ),
1818 ] {
1819 let mut map = map_with_nodes();
1820 apply(map.node(1).unwrap());
1821 let recorded = map.notifications.get(&1).expect("effect must be recorded");
1822 assert_eq!(recorded.animation, expected);
1823 assert_eq!(recorded.started, now);
1824 assert!(map.node_states.is_empty());
1826 }
1827 }
1828
1829 #[test]
1830 fn each_steady_effect_records_lasting_state() {
1831 for (apply, expected) in [
1832 (
1833 Box::new(|n: NodeHandle| n.halo()) as Box<dyn FnOnce(NodeHandle)>,
1834 SteadyAnimation::Halo,
1835 ),
1836 (Box::new(|n: NodeHandle| n.blink()), SteadyAnimation::Blink),
1837 (Box::new(|n: NodeHandle| n.orbit()), SteadyAnimation::Orbit),
1838 ] {
1839 let mut map = map_with_nodes();
1840 apply(map.node(1).unwrap());
1841 assert_eq!(map.node_states.get(&1).unwrap().animation, expected);
1842 assert!(map.notifications.is_empty());
1844 }
1845 }
1846
1847 #[test]
1848 fn color_modifier_reaches_both_families() {
1849 let mut map = map_with_nodes();
1850 map.node(1)
1851 .unwrap()
1852 .color(Color32::RED)
1853 .pulse(Instant::now());
1854 map.node(2).unwrap().color(Color32::BLUE).halo();
1855
1856 assert_eq!(map.notifications.get(&1).unwrap().color, Some(Color32::RED));
1857 assert_eq!(map.node_states.get(&2).unwrap().color, Some(Color32::BLUE));
1858 }
1859
1860 #[test]
1861 fn a_node_can_carry_state_and_a_notification_at_once() {
1862 let mut map = map_with_nodes();
1863 map.node(1).unwrap().halo();
1864 map.node(1).unwrap().ripple(Instant::now());
1865
1866 assert!(map.node_states.contains_key(&1));
1867 assert!(map.notifications.contains_key(&1));
1868 }
1869
1870 #[test]
1871 fn clear_removes_both_families_for_that_node_only() {
1872 let mut map = map_with_nodes();
1873 map.node(1).unwrap().halo();
1874 map.node(1).unwrap().ripple(Instant::now());
1875 map.node(2).unwrap().halo();
1876
1877 map.node(1).unwrap().clear();
1878
1879 assert!(!map.node_states.contains_key(&1));
1880 assert!(!map.notifications.contains_key(&1));
1881 assert!(map.node_states.contains_key(&2), "node 2 must be untouched");
1882 }
1883
1884 #[test]
1885 fn re_triggering_replaces_the_previous_effect() {
1886 let mut map = map_with_nodes();
1887 map.node(1).unwrap().pulse(Instant::now());
1888 map.node(1).unwrap().crosshair(Instant::now());
1889
1890 assert_eq!(map.notifications.len(), 1);
1891 assert_eq!(
1892 map.notifications.get(&1).unwrap().animation,
1893 NodeAnimation::Crosshair
1894 );
1895 }
1896
1897 #[test]
1898 fn update_marker_inserts_and_updates() {
1899 let mut map = Map::new();
1900 map.update_marker(1, 100);
1901 assert_eq!(map.markers.get(&1), Some(&100));
1902 map.update_marker(1, 200);
1903 assert_eq!(map.markers.get(&1), Some(&200));
1904 assert_eq!(map.markers.len(), 1);
1905 }
1906
1907 #[test]
1910 fn allocate_at_least_sets_min_size() {
1911 let mut map = Map::new();
1912 map.allocate_at_least(Some(100.0), None);
1913 assert_eq!(map.min_size, (Some(100.0), None));
1914 }
1915
1916 #[test]
1917 fn allocate_at_most_sets_max_size() {
1918 let mut map = Map::new();
1919 map.allocate_at_most(None, Some(200.0));
1920 assert_eq!(map.max_size, (None, Some(200.0)));
1921 }
1922
1923 #[test]
1926 fn adjust_bounds_scales_with_zoom() {
1927 let mut map = Map::new();
1928 map.reference.min = RawPoint::new(-10.0, -20.0);
1929 map.reference.max = RawPoint::new(10.0, 20.0);
1930 map.reference.pos = RawPoint::new(5.0, 5.0);
1931 map.reference.dist = 100.0;
1932 map.set_zoom(2.0);
1933 map.adjust_bounds();
1934
1935 assert_eq!(map.current.max.components, [20.0, 40.0]);
1936 assert_eq!(map.current.min.components, [-20.0, -40.0]);
1937 assert_eq!(map.current.pos.components, [10.0, 10.0]);
1938 assert_eq!(map.current.dist, 50.0);
1939 }
1940}