1use crate::map::animation::Animation;
75use crate::map::objects::{
76 ContextMenuManager, MapBounds, MapLabel, MapLine, MapPoint, MapSettings, MapStyle, RawLine,
77 RawPoint, TextSettings, VisibilitySetting,
78};
79use egui::{epaint::CircleShape, widgets::*, *};
80use kdtree::KdTree;
81use kdtree::distance::squared_euclidean;
82use std::collections::{HashMap, HashSet};
83use std::rc::Rc;
84use std::time::Instant;
85
86use self::objects::NodeTemplate;
87
88pub mod animation;
89pub mod objects;
90
91#[derive(Clone)]
129pub struct Map {
130 zoom: f32,
131 previous_zoom: f32,
132 points: Option<HashMap<usize, MapPoint>>,
133 lines: Option<HashMap<Rc<str>, MapLine>>,
134 labels: Vec<MapLabel>,
135 tree: Option<KdTree<f32, usize, [f32; 2]>>,
136 visible_points: Vec<isize>,
137 map_area: Rect,
138 reference: MapBounds,
139 current: MapBounds,
140 current_index: usize,
141 entities: HashMap<usize, Instant>,
142 min_size: (Option<f32>, Option<f32>),
143 max_size: (Option<f32>, Option<f32>),
144 pub settings: MapSettings,
147 menu_manager: Option<Rc<dyn ContextMenuManager>>,
148 node_template: Option<Rc<dyn NodeTemplate>>,
149 visible_lines: HashSet<Rc<str>>,
150 markers: HashMap<usize, usize>,
151}
152
153impl Default for Map {
154 fn default() -> Self {
156 Map::new()
157 }
158}
159
160impl Widget for &mut Map {
161 fn ui(self, ui: &mut egui::Ui) -> Response {
164 let rect = self.calculate_widget_dimensions(ui);
165
166 self.reference.dist = rect.distance();
168
169 self.assign_visual_style(ui);
170
171 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
172
173 let inner_response = canvas.show(ui, |ui| {
174 #[cfg(feature = "puffin")]
175 puffin::profile_scope!("paint_map");
176
177 if ui.is_rect_visible(self.map_area) {
178 let (resp, paint) =
179 ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
180 let vec = resp.drag_delta();
181 if vec.length() != 0.0 {
182 #[cfg(feature = "puffin")]
183 puffin::profile_scope!("calculating_points_in_visible_area");
184
185 let coords = RawPoint::from(vec.to_pos2());
186 let new_pos = self.reference.pos - (coords / self.zoom);
187 self.set_pos(new_pos.into());
188 }
189 if self.zoom < self.settings.line_visible_zoom {
190 let mut text_settings = TextSettings {
192 size: 12.00 * self.zoom * 2.00,
193 anchor: Align2::CENTER_CENTER,
194 family: FontFamily::Proportional,
195 text: String::new(),
196 position: RawPoint::default(),
197 text_color: ui.visuals().text_color(),
198 };
199 for label in &self.labels {
200 text_settings.text.clone_from(&label.text);
201 text_settings.position = RawPoint::from(label.center);
202 self.paint_label(&paint, &text_settings);
203 }
204 }
205
206 let rect_midpoint = RawPoint::from(self.map_area.center());
207 let min_point = self.current.pos - rect_midpoint;
208 let vec_points = &self.visible_points;
209 let hashm = &self.points;
210
211 let now = Instant::now();
214 self.entities
215 .retain(|_, init| now.duration_since(*init).as_secs_f32() < 10.0);
216
217 self.paint_map_lines(&paint, &min_point);
218
219 if let Ok(nodes_to_remove) =
220 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
221 {
222 for node in nodes_to_remove {
223 self.entities.remove(&node);
224 }
225 }
226
227 for marker in &self.markers {
228 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
229 let adjusted_point = point.raw_point * self.zoom - min_point;
230 if let Some(template) = &self.node_template {
231 template.marker_ui(ui, adjusted_point.into(), self.zoom);
232 } else {
233 let mut shapes = Vec::new();
234 let color = if ui.visuals().dark_mode {
235 Color32::LIGHT_GREEN
236 } else {
237 Color32::GREEN
238 };
239 let millis = std::time::SystemTime::now()
240 .duration_since(std::time::UNIX_EPOCH)
241 .unwrap_or_default()
242 .as_millis();
243 let mut transparency = (millis % 2550 / 5) as i64;
244 if transparency > 255 {
245 transparency = 255 - (transparency - 255)
246 }
247 let corrected_color = Color32::from_rgba_unmultiplied(
248 color.r(),
249 color.g(),
250 color.b(),
251 transparency as u8,
252 );
253 shapes.push(Shape::Circle(CircleShape::stroke(
254 adjusted_point.into(),
255 4.0 * self.zoom,
256 Stroke::new(9.0 * self.zoom, corrected_color),
257 )));
258 ui.ctx().request_repaint();
259 ui.painter().extend(shapes);
260 }
261 }
262 }
263
264 self.paint_sub_components(ui, self.map_area);
265
266 self.capture_mouse_events(ui, &resp);
267
268 if self.zoom != self.previous_zoom {
269 #[cfg(feature = "puffin")]
270 puffin::profile_scope!("calculating viewport with zoom");
271 self.adjust_bounds();
272 self.calculate_visible_points();
273 self.previous_zoom = self.zoom;
274 }
275
276 if let Some(menu_mon) = &mut self.menu_manager {
277 resp.context_menu(|ui| {
278 menu_mon.ui(ui);
279 });
280 }
281
282 if cfg!(debug_assertions) {
283 self.print_debug_info(paint, resp);
284 }
285 }
286 });
287 ui.allocate_space(self.map_area.size());
288 inner_response.response
289 }
290}
291
292impl Map {
293 pub fn new() -> Self {
298 let settings = MapSettings::default();
299 Self {
300 zoom: 1.0,
301 previous_zoom: 1.0,
302 map_area: Rect::NOTHING,
303 tree: None,
304 points: None,
305 lines: None,
306 labels: Vec::new(),
307 visible_points: Vec::new(),
308 current: MapBounds::default(),
309 reference: MapBounds::default(),
310 settings,
311 min_size: (None, None),
312 max_size: (None, None),
313 current_index: 0,
314 entities: HashMap::new(),
315 menu_manager: None,
316 node_template: None,
317 visible_lines: HashSet::new(),
318 markers: HashMap::new(),
319 }
320 }
321
322 fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
323 let available = ui.available_rect_before_wrap();
324 let mut size = available.size();
325 if let Some(max_width) = self.max_size.0 {
326 size.x = size.x.min(max_width);
327 }
328 if let Some(max_height) = self.max_size.1 {
329 size.y = size.y.min(max_height);
330 }
331 if let Some(min_width) = self.min_size.0 {
332 size.x = size.x.max(min_width);
333 }
334 if let Some(min_height) = self.min_size.1 {
335 size.y = size.y.max(min_height);
336 }
337 self.map_area = Rect::from_min_size(available.min, size);
338 RawLine::new(
339 RawPoint::from(self.map_area.left_top()),
340 RawPoint::from(self.map_area.right_bottom()),
341 )
342 }
343
344 fn calculate_visible_points(&mut self) {
345 #[cfg(feature = "puffin")]
346 puffin::profile_scope!("calculate_visible_points");
347 if self.current.dist > 0.0
348 && self.current.dist < f32::INFINITY
349 && let Some(tree) = &self.tree
350 {
351 let center = self.current.pos / self.zoom;
352 let radius = self.current.dist.powi(2);
353 let point: [f32; 2] = center.into();
354 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
355 self.visible_points.clear();
356 self.visible_lines.clear();
357 for point in vis_pos {
358 self.visible_points.push(point.1.cast_signed());
359 let system = self.points.as_ref().unwrap().get(point.1);
360 for connection in &system.unwrap().connections {
361 if let Some((key, _)) = self
364 .lines
365 .as_ref()
366 .and_then(|lines| lines.get_key_value(connection.as_str()))
367 {
368 self.visible_lines.insert(Rc::clone(key));
369 }
370 }
371 }
372 }
373 }
374
375 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
404 #[cfg(feature = "puffin")]
405 puffin::profile_scope!("add_hashmap_points");
406 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
407 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
408 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
409
410 for entry in hash_map.iter() {
411 for i in 0..min.components.len() {
412 if entry.1.raw_point.components[i] < min.components[i] {
413 min.components[i] = entry.1.raw_point.components[i];
414 }
415 if entry.1.raw_point.components[i] > max.components[i] {
416 max.components[i] = entry.1.raw_point.components[i];
417 }
418 }
419 let _result = tree.add(entry.1.raw_point.into(), *entry.0);
420 }
421
422 self.reference.min = min;
424 self.reference.max = max;
425 self.points = Some(hash_map);
426 self.tree = Some(tree);
427 self.reference.pos = RawLine::new(min, max).midpoint();
428 if self.map_area.area() == 0.0 {
432 self.reference.dist = 3000.00;
433 } else {
434 let rect = RawLine::new(
435 RawPoint::from(self.map_area.left_top()),
436 RawPoint::from(self.map_area.right_bottom()),
437 );
438 self.reference.dist = rect.distance();
439 }
440 self.current = self.reference.clone();
441 self.calculate_visible_points();
442 }
443
444 pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
449 #[cfg(feature = "puffin")]
450 puffin::profile_scope!("set_pos_from_nodeid");
451 if let Some(hash_map) = &self.points
452 && let Some(map_point) = hash_map.get(&node_id)
453 {
454 self.reference.pos = map_point.raw_point;
455 self.adjust_bounds();
456 self.calculate_visible_points();
457 }
458 }
459
460 pub fn set_pos(&mut self, position: [f32; 2]) {
462 #[cfg(feature = "puffin")]
463 puffin::profile_scope!("set_pos");
464 let point = RawPoint::from(position);
465 self.reference.pos = point;
466 self.adjust_bounds();
467 self.calculate_visible_points();
468 }
469
470 pub fn get_pos(&self) -> [f32; 2] {
472 #[cfg(feature = "puffin")]
473 puffin::profile_scope!("get_pos");
474 self.reference.pos.into()
475 }
476
477 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
482 #[cfg(feature = "puffin")]
483 puffin::profile_scope!("add_labels");
484 self.labels = labels;
485 }
486
487 pub fn add_lines(&mut self, lines: HashMap<String, MapLine>) {
498 #[cfg(feature = "puffin")]
499 puffin::profile_scope!("add_lines");
500 self.lines = Some(
503 lines
504 .into_iter()
505 .map(|(key, line)| (Rc::from(key.as_str()), line))
506 .collect(),
507 );
508 self.calculate_visible_points();
514 }
515
516 fn adjust_bounds(&mut self) {
517 #[cfg(feature = "puffin")]
518 puffin::profile_scope!("adjust_bounds");
519 self.current.max = self.reference.max * self.zoom;
520 self.current.min = self.reference.min * self.zoom;
521 self.current.dist = self.reference.dist / self.zoom;
522 self.current.pos = self.reference.pos * self.zoom;
523 }
524
525 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
526 #[cfg(feature = "puffin")]
527 puffin::profile_scope!("capture_mouse_events");
528 if ui.rect_contains_pointer(self.map_area) {
530 ui.input(|x| {
531 #[cfg(feature = "puffin")]
532 puffin::profile_scope!("capture_mouse_events");
533
534 if !x.events.is_empty() {
535 for event in &x.events {
536 match event {
537 Event::MouseWheel {
538 unit: _,
539 delta,
540 modifiers,
541 phase: _,
542 } => {
543 #[cfg(target_os = "macos")]
544 let zoom_modifier = if modifiers.mac_cmd {
545 delta.y / 80.00
546 } else {
547 delta.y / 400.00
548 };
549
550 #[cfg(not(target_os = "macos"))]
551 let zoom_modifier = if modifiers.ctrl {
552 delta.y / 8.00
553 } else {
554 delta.y / 40.00
555 };
556
557 let mut pre_zoom = self.zoom + zoom_modifier;
558 if pre_zoom > self.settings.max_zoom {
559 pre_zoom = self.settings.max_zoom;
560 }
561 if pre_zoom < self.settings.min_zoom {
562 pre_zoom = self.settings.min_zoom;
563 }
564 self.zoom = pre_zoom;
565 }
566 _ => {
567 continue;
568 }
569 };
570 }
571 }
572 });
573 }
574 }
575
576 pub fn set_zoom(&mut self, value: f32) {
581 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
582 self.zoom = value;
583 }
584 }
585
586 pub fn get_zoom(&mut self) -> f32 {
588 self.zoom
589 }
590
591 fn current_style(&self) -> &MapStyle {
594 self.settings
595 .styles
596 .get(self.current_index)
597 .or(self.settings.styles.first())
598 .expect("MapSettings::styles must not be empty")
599 }
600
601 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
602 let style_index = ui_obj.visuals().dark_mode as usize;
603
604 if self.current_index != style_index {
605 #[cfg(feature = "puffin")]
606 puffin::profile_scope!("asign_visual_style");
607
608 self.current_index = style_index;
609 let map_style = self.current_style();
610 let visuals = &mut ui_obj.style_mut().visuals;
611 visuals.extreme_bg_color = map_style.background_color;
612 if let Some(border) = map_style.border {
613 visuals.window_stroke = border;
614 }
615 }
616 }
617
618 fn print_debug_info(&mut self, paint: Painter, resp: Response) {
619 #[cfg(feature = "puffin")]
620 puffin::profile_scope!("printing debug data");
621
622 let mut init_pos = Pos2::new(
623 self.map_area.left_top().x + 10.00,
624 self.map_area.left_top().y + 10.00,
625 );
626 let mut msg = "MIN:".to_string()
627 + self.current.min.components[0].to_string().as_str()
628 + ","
629 + self.current.min.components[1].to_string().as_str();
630 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
631 init_pos.y += 15.0;
632 msg = "MAX:".to_string()
633 + self.current.max.components[0].to_string().as_str()
634 + ","
635 + self.current.max.components[1].to_string().as_str();
636 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
637 init_pos.y += 15.0;
638 msg = "CUR:(".to_string()
639 + self.current.pos.components[0].to_string().as_str()
640 + ","
641 + self.current.pos.components[1].to_string().as_str()
642 + ")";
643 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
644 init_pos.y += 15.0;
645 msg = "DST:".to_string() + self.current.dist.to_string().as_str();
646 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
647 init_pos.y += 15.0;
648 msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
649 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
650 init_pos.y += 15.0;
651 msg = "REC:(".to_string()
652 + self.map_area.left_top().x.to_string().as_str()
653 + ","
654 + self.map_area.left_top().y.to_string().as_str()
655 + "),("
656 + self.map_area.right_bottom().x.to_string().as_str()
657 + ","
658 + self.map_area.right_bottom().y.to_string().as_str()
659 + ")";
660 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
661 if let Some(points) = &self.points {
662 init_pos.y += 15.0;
663 msg = "NUM:".to_string() + points.len().to_string().as_str();
664 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
665 }
666 if !self.visible_points.is_empty() {
667 init_pos.y += 15.0;
668 msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
669 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
670 }
671 if let Some(pointer_pos) = resp.hover_pos() {
672 init_pos.y += 15.0;
673 msg = "HVR:".to_string()
674 + pointer_pos.x.to_string().as_str()
675 + ","
676 + pointer_pos.y.to_string().as_str();
677 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
678 }
679 let vec = resp.drag_delta();
680 if vec.length() != 0.0 {
681 init_pos.y += 15.0;
682 msg = "DRG:".to_string()
683 + vec.to_pos2().x.to_string().as_str()
684 + ","
685 + vec.to_pos2().y.to_string().as_str();
686 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
687 }
688 }
689
690 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
691 #[cfg(feature = "puffin")]
692 puffin::profile_scope!("map_ui_paint_sub_components");
693 let zoom_slider = egui::Slider::new(
694 &mut self.zoom,
695 self.settings.min_zoom..=self.settings.max_zoom,
696 )
697 .show_value(false)
698 .orientation(SliderOrientation::Vertical);
699 let mut pos1 = rect.right_top();
700 let mut pos2 = rect.right_top();
701 pos1.x -= 80.0;
702 pos1.y += 120.0;
703 pos2.x -= 60.0;
704 pos2.y += 240.0;
705
706 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
707 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
708 ui_obj.scope_builder(ui_builder, |ui_obj| {
709 ui_obj.add(zoom_slider);
710 });
711 }
712
713 fn paint_map_points(
714 &self,
715 vec_points: &Vec<isize>,
716 hashm: &Option<HashMap<usize, MapPoint>>,
717 paint: &Painter,
718 ui_obj: &mut Ui,
719 min_point: &RawPoint,
720 resp: &Response,
721 ) -> Result<Vec<usize>, ()> {
722 let mut nearest_id = None;
723 let mut nodes_to_remove = Vec::new();
724 let mut shape_vec = vec![];
725
726 if hashm.is_none() {
727 return Err(());
728 }
729 if vec_points.is_empty() {
730 return Err(());
731 }
732 if self.settings.node_text_visibility == VisibilitySetting::Hover
734 && resp.hovered()
735 && let Some(point) = resp.hover_pos()
736 {
737 let raw_point = RawPoint::from(point);
738 let hovered_map_point = (*min_point + raw_point) / self.zoom;
739 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
740 &hovered_map_point.components,
741 1,
742 &squared_euclidean,
743 ) {
744 nearest_id = Some(nearest_node.first().unwrap().1);
745 }
746 }
747 let mut text_settings = TextSettings {
749 size: 12.00 * self.zoom,
750 anchor: Align2::LEFT_BOTTOM,
751 family: FontFamily::Proportional,
752 text: String::new(),
753 position: RawPoint::default(),
754 text_color: ui_obj.visuals().text_color(),
755 };
756
757 for temp_point in vec_points {
759 let parsed_point = temp_point.cast_unsigned();
760 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
761 #[cfg(feature = "puffin")]
762 puffin::profile_scope!("painting_points_m");
763 let viewport_point = system.raw_point * self.zoom - min_point;
764 if let Some(node_template) = &self.node_template {
765 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
766 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
767 }
768 } else if self.zoom > self.settings.label_visible_zoom
769 && self.settings.node_text_visibility == VisibilitySetting::Always
770 || (self.settings.node_text_visibility == VisibilitySetting::Hover
771 && nearest_id.unwrap_or(&0usize) == &system.get_id())
772 {
773 let mut viewport_text = viewport_point;
774 viewport_text.components[0] += 3.0 * self.zoom;
775 viewport_text.components[1] -= 3.0 * self.zoom;
776 text_settings.position = viewport_text;
777 text_settings.text = system.get_name();
778 self.paint_label(paint, &text_settings);
779 }
780
781 let system_id = system.get_id();
782 if let Some(init_time) = self.entities.get(&system_id) {
783 if let Some(template) = &self.node_template {
784 template.notification_ui(
785 ui_obj,
786 viewport_point.into(),
787 self.zoom,
788 *init_time,
789 self.current_style().alert_color,
790 );
791 } else if Animation::pulse(
792 paint,
793 viewport_point,
794 self.zoom,
795 *init_time,
796 self.current_style().alert_color,
797 ) {
798 ui_obj.ctx().request_repaint();
799 } else {
800 nodes_to_remove.push(system_id);
801 }
802 }
803 if let Some(node_template) = &self.node_template {
804 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
805 } else {
806 shape_vec.push(Shape::circle_filled(
807 viewport_point.into(),
808 4.00 * self.zoom,
809 self.current_style().fill_color,
810 ));
811 }
812 }
813 }
814 paint.extend(shape_vec);
815 Ok(nodes_to_remove)
816 }
817
818 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
819 #[cfg(feature = "puffin")]
820 puffin::profile_scope!("paint_map_lines");
821
822 if self.zoom > self.settings.line_visible_zoom
824 && let Some(mut stroke) = self.current_style().line
825 {
826 let mut shape_vec = vec![];
827 let transparency_range = self.zoom - self.settings.line_visible_zoom;
828 if (0.00..0.80).contains(&transparency_range) {
829 let mut tup_stroke = stroke.color.to_tuple();
830 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
831 tup_stroke.3 = (255.0 * transparency).round() as u8;
832 let color = Color32::from_rgba_unmultiplied(
833 tup_stroke.0,
834 tup_stroke.1,
835 tup_stroke.2,
836 tup_stroke.3,
837 );
838 stroke = Stroke::new(stroke.width, color);
839 }
840 for line in &self.visible_lines {
841 if let Some(connection) = self.lines.as_ref().unwrap().get(line) {
842 let pos_a = connection.raw_line.points[0] * self.zoom - min_point;
843 let pos_b = connection.raw_line.points[1] * self.zoom - min_point;
844 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
845 }
846 }
847 painter.extend(shape_vec);
848 }
849 }
850
851 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
852 #[cfg(feature = "puffin")]
853 puffin::profile_scope!("paint_label");
854 paint.text(
855 text_settings.position.into(),
856 text_settings.anchor,
857 text_settings.text.clone(),
858 FontId::new(text_settings.size, text_settings.family.clone()),
859 text_settings.text_color,
860 );
861 }
862
863 pub fn notify(&mut self, id_node: usize, time: Instant) {
870 #[cfg(feature = "puffin")]
871 puffin::profile_scope!("notify");
872 self.entities
873 .entry(id_node)
874 .and_modify(|value| *value = time)
875 .or_insert(time);
876 }
877
878 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
881 self.menu_manager = Some(manager);
882 }
883
884 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
892 self.node_template = Some(template);
893 }
894
895 pub fn update_marker(&mut self, id: usize, node_id: usize) {
900 self.markers
901 .entry(id)
902 .and_modify(|value| *value = node_id)
903 .or_insert(node_id);
904 }
905
906 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
909 self.min_size = (width, height);
910 }
911
912 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
915 self.max_size = (width, height);
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922 use std::time::Duration;
923
924 fn sample_points() -> HashMap<usize, MapPoint> {
925 let mut map = HashMap::new();
926 map.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
927 map.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
928 map.insert(3, MapPoint::new(3, RawPoint::new(-10.0, -10.0)));
929 map
930 }
931
932 #[test]
935 fn map_new_initial_state() {
936 let map = Map::new();
937 assert_eq!(map.zoom, 1.0);
938 assert_eq!(map.previous_zoom, 1.0);
939 assert!(map.points.is_none());
940 assert!(map.lines.is_none());
941 assert!(map.tree.is_none());
942 assert!(map.labels.is_empty());
943 assert!(map.visible_points.is_empty());
944 assert!(map.visible_lines.is_empty());
945 assert!(map.markers.is_empty());
946 assert!(map.entities.is_empty());
947 assert_eq!(map.min_size, (None, None));
948 assert_eq!(map.max_size, (None, None));
949 assert_eq!(map.current_index, 0);
950 }
951
952 #[test]
953 fn map_default_equals_new() {
954 let map = Map::default();
955 assert_eq!(map.zoom, 1.0);
956 assert!(map.points.is_none());
957 }
958
959 #[test]
962 fn set_zoom_within_range() {
963 let mut map = Map::new();
964 map.set_zoom(1.5);
965 assert_eq!(map.get_zoom(), 1.5);
966 }
967
968 #[test]
969 fn set_zoom_at_exact_limits() {
970 let mut map = Map::new();
971 map.set_zoom(map.settings.min_zoom);
972 assert_eq!(map.get_zoom(), 0.1);
973 map.set_zoom(map.settings.max_zoom);
974 assert_eq!(map.get_zoom(), 2.0);
975 }
976
977 #[test]
978 fn set_zoom_out_of_range_is_ignored() {
979 let mut map = Map::new();
980 let initial = map.get_zoom();
981 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
983 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
985 }
986
987 #[test]
990 fn add_hashmap_points_computes_bounds() {
991 let mut map = Map::new();
992 map.add_hashmap_points(sample_points());
993
994 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
995 assert_eq!(map.reference.max.components, [10.0, 10.0]);
996 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
998 assert_eq!(map.reference.dist, 3000.0);
1000 assert_eq!(map.current.min.components, map.reference.min.components);
1002 assert_eq!(map.current.max.components, map.reference.max.components);
1003 assert_eq!(map.current.pos.components, map.reference.pos.components);
1004 assert_eq!(map.current.dist, map.reference.dist);
1005 assert!(map.points.is_some());
1006 assert!(map.tree.is_some());
1007 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1008 }
1009
1010 #[test]
1011 fn add_hashmap_points_populates_visible_points() {
1012 let mut map = Map::new();
1013 map.add_hashmap_points(sample_points());
1014 assert_eq!(map.visible_points.len(), 3);
1016 }
1017
1018 #[test]
1019 fn add_hashmap_points_populates_visible_lines() {
1020 let mut points = sample_points();
1021 points
1022 .get_mut(&1)
1023 .unwrap()
1024 .connections
1025 .push("1-2".to_string());
1026 let mut lines = HashMap::new();
1027 lines.insert(
1028 "1-2".to_string(),
1029 MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0)),
1030 );
1031 let mut map = Map::new();
1032 map.add_lines(lines);
1033 map.add_hashmap_points(points);
1034 assert!(map.visible_lines.contains("1-2"));
1035 }
1036
1037 #[test]
1038 fn add_lines_after_points_populates_visible_lines() {
1039 let mut points = sample_points();
1043 points
1044 .get_mut(&1)
1045 .unwrap()
1046 .connections
1047 .push("1-2".to_string());
1048 let mut map = Map::new();
1049 map.add_hashmap_points(points);
1050
1051 let mut lines = HashMap::new();
1052 lines.insert(
1053 "1-2".to_string(),
1054 MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0)),
1055 );
1056 map.add_lines(lines);
1057
1058 assert!(map.visible_lines.contains("1-2"));
1059 }
1060
1061 #[test]
1062 fn map_check_line_is_painted_on_first_frame() {
1063 use egui::{Context, RawInput, Shape};
1064
1065 let mut map = Map::new();
1067 map.set_zoom(1.0);
1068
1069 let mut point_a = MapPoint::new(0, RawPoint::new(0.0, 0.0));
1070 point_a.connections.push("a0".to_string());
1071 let mut point_b = MapPoint::new(1, RawPoint::new(50.0, 50.0));
1072 point_b.connections.push("a0".to_string());
1073
1074 let mut lines = HashMap::new();
1075 lines.insert(
1076 "a0".to_string(),
1077 MapLine::new(point_a.raw_point, point_b.raw_point),
1078 );
1079
1080 let mut points = HashMap::new();
1081 points.insert(0usize, point_a);
1082 points.insert(1usize, point_b);
1083 map.add_hashmap_points(points);
1085 map.add_lines(lines);
1086
1087 map.set_pos([25.0, 25.0]);
1088
1089 assert!(
1090 map.visible_lines.contains("a0"),
1091 "visible_lines must contain \"a0\" after add_hashmap_points"
1092 );
1093
1094 let ctx = Context::default();
1096 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1097 let input = RawInput {
1098 screen_rect: Some(screen),
1099 ..RawInput::default()
1100 };
1101
1102 let output1 = ctx.run_ui(input.clone(), |ui| {
1103 ui.add(&mut map);
1104 });
1105
1106 let segments1: Vec<[egui::Pos2; 2]> = output1
1107 .shapes
1108 .iter()
1109 .filter_map(|cs| match cs.shape {
1110 Shape::LineSegment { points, .. } => Some(points),
1111 _ => None,
1112 })
1113 .collect();
1114
1115 assert!(
1116 !segments1.is_empty(),
1117 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
1118 );
1119
1120 let expected_a = egui::pos2(225.0, 225.0);
1123 let expected_b = egui::pos2(275.0, 275.0);
1124 let tolerance = 2.0;
1125 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
1126 let d_a1 = p1.distance(expected_a);
1127 let d_b1 = p2.distance(expected_b);
1128 let d_a2 = p2.distance(expected_a);
1129 let d_b2 = p1.distance(expected_b);
1130 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
1131 });
1132 assert!(
1133 found_on_frame1,
1134 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
1135 segments1
1136 );
1137
1138 let output2 = ctx.run_ui(input, |ui| {
1140 ui.add(&mut map);
1141 });
1142
1143 let segments2: Vec<[egui::Pos2; 2]> = output2
1144 .shapes
1145 .iter()
1146 .filter_map(|cs| match cs.shape {
1147 Shape::LineSegment { points, .. } => Some(points),
1148 _ => None,
1149 })
1150 .collect();
1151
1152 assert_eq!(
1153 segments1.len(),
1154 segments2.len(),
1155 "Frame 2: expected {} line segments (no duplication across frames), got {}",
1156 segments1.len(),
1157 segments2.len()
1158 );
1159 }
1160
1161 #[test]
1164 fn set_pos_and_get_pos_roundtrip() {
1165 let mut map = Map::new();
1166 map.set_pos([25.0, -35.0]);
1167 assert_eq!(map.get_pos(), [25.0, -35.0]);
1168 }
1169
1170 #[test]
1171 fn set_pos_from_nodeid_with_valid_id() {
1172 let mut map = Map::new();
1173 map.add_hashmap_points(sample_points());
1174 map.set_pos_from_nodeid(2);
1175 assert_eq!(map.get_pos(), [10.0, 10.0]);
1176 }
1177
1178 #[test]
1179 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1180 let mut map = Map::new();
1181 map.add_hashmap_points(sample_points());
1182 let before = map.reference.pos.components;
1183 map.set_pos_from_nodeid(999);
1184 assert_eq!(map.reference.pos.components, before);
1185 }
1186
1187 #[test]
1188 fn set_pos_from_nodeid_without_points_does_nothing() {
1189 let mut map = Map::new();
1190 map.set_pos_from_nodeid(1);
1191 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1192 }
1193
1194 #[test]
1197 fn add_labels_stores_labels() {
1198 let mut map = Map::new();
1199 let label = MapLabel {
1200 text: "Region".to_string(),
1201 center: Pos2::new(1.0, 2.0),
1202 };
1203 map.add_labels(vec![label]);
1204 assert_eq!(map.labels.len(), 1);
1205 assert_eq!(map.labels[0].text, "Region");
1206 }
1207
1208 #[test]
1209 fn add_lines_stores_lines() {
1210 let mut map = Map::new();
1211 let mut lines = HashMap::new();
1212 lines.insert(
1213 "a-b".to_string(),
1214 MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(1.0, 1.0)),
1215 );
1216 map.add_lines(lines);
1217 assert!(map.lines.as_ref().unwrap().contains_key("a-b"));
1218 }
1219
1220 #[test]
1223 fn notify_inserts_and_updates_entities() {
1224 let mut map = Map::new();
1225 let t1 = Instant::now();
1226 map.notify(5, t1);
1227 assert_eq!(map.entities.get(&5), Some(&t1));
1228
1229 let t2 = t1 + Duration::from_secs(1);
1230 map.notify(5, t2);
1231 assert_eq!(map.entities.get(&5), Some(&t2));
1232 assert_eq!(map.entities.len(), 1);
1233 }
1234
1235 #[test]
1236 fn update_marker_inserts_and_updates() {
1237 let mut map = Map::new();
1238 map.update_marker(1, 100);
1239 assert_eq!(map.markers.get(&1), Some(&100));
1240 map.update_marker(1, 200);
1241 assert_eq!(map.markers.get(&1), Some(&200));
1242 assert_eq!(map.markers.len(), 1);
1243 }
1244
1245 #[test]
1248 fn allocate_at_least_sets_min_size() {
1249 let mut map = Map::new();
1250 map.allocate_at_least(Some(100.0), None);
1251 assert_eq!(map.min_size, (Some(100.0), None));
1252 }
1253
1254 #[test]
1255 fn allocate_at_most_sets_max_size() {
1256 let mut map = Map::new();
1257 map.allocate_at_most(None, Some(200.0));
1258 assert_eq!(map.max_size, (None, Some(200.0)));
1259 }
1260
1261 #[test]
1264 fn adjust_bounds_scales_with_zoom() {
1265 let mut map = Map::new();
1266 map.reference.min = RawPoint::new(-10.0, -20.0);
1267 map.reference.max = RawPoint::new(10.0, 20.0);
1268 map.reference.pos = RawPoint::new(5.0, 5.0);
1269 map.reference.dist = 100.0;
1270 map.set_zoom(2.0);
1271 map.adjust_bounds();
1272
1273 assert_eq!(map.current.max.components, [20.0, 40.0]);
1274 assert_eq!(map.current.min.components, [-20.0, -40.0]);
1275 assert_eq!(map.current.pos.components, [10.0, 10.0]);
1276 assert_eq!(map.current.dist, 50.0);
1277 }
1278}