1use crate::map::animation::Animation;
72use crate::map::objects::{
73 ContextMenuManager, MapBounds, MapLabel, MapPoint, MapSegment, MapSettings, MapStyle, RawLine,
74 RawPoint, TextSettings, VisibilitySetting,
75};
76use egui::{epaint::CircleShape, 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 entities: HashMap<usize, Instant>,
139 min_size: (Option<f32>, Option<f32>),
140 max_size: (Option<f32>, Option<f32>),
141 pub settings: MapSettings,
144 menu_manager: Option<Rc<dyn ContextMenuManager>>,
145 node_template: Option<Rc<dyn NodeTemplate>>,
146 markers: HashMap<usize, usize>,
147}
148
149impl Default for Map {
150 fn default() -> Self {
152 Map::new()
153 }
154}
155
156impl Widget for &mut Map {
157 fn ui(self, ui: &mut egui::Ui) -> Response {
160 let rect = self.calculate_widget_dimensions(ui);
161
162 self.reference.dist = rect.distance();
164
165 self.assign_visual_style(ui);
166
167 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
168
169 let inner_response = canvas.show(ui, |ui| {
170 profiling::scope!("paint_map");
171
172 if ui.is_rect_visible(self.map_area) {
173 let (resp, paint) =
174 ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
175 let vec = resp.drag_delta();
176 if vec.length() != 0.0 {
177 profiling::scope!("calculating_points_in_visible_area");
178
179 let coords = RawPoint::from(vec.to_pos2());
180 let new_pos = self.reference.pos - (coords / self.zoom);
181 self.set_pos(new_pos.into());
182 }
183 if self.zoom < self.settings.line_visible_zoom {
184 let mut text_settings = TextSettings {
186 size: 12.00 * self.zoom * 2.00,
187 anchor: Align2::CENTER_CENTER,
188 family: FontFamily::Proportional,
189 text: String::new(),
190 position: RawPoint::default(),
191 text_color: ui.visuals().text_color(),
192 };
193 for label in &self.labels {
194 text_settings.text.clone_from(&label.text);
195 text_settings.position = RawPoint::from(label.center);
196 self.paint_label(&paint, &text_settings);
197 }
198 }
199
200 let rect_midpoint = RawPoint::from(self.map_area.center());
201 let min_point = self.current.pos - rect_midpoint;
202 let vec_points = &self.visible_points;
203 let hashm = &self.points;
204
205 let now = Instant::now();
208 self.entities
209 .retain(|_, init| now.duration_since(*init).as_secs_f32() < 10.0);
210
211 self.paint_map_lines(&paint, &min_point);
212
213 if let Ok(nodes_to_remove) =
214 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
215 {
216 for node in nodes_to_remove {
217 self.entities.remove(&node);
218 }
219 }
220
221 for marker in &self.markers {
222 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
223 let adjusted_point = RawPoint::from(point.coords) * self.zoom - min_point;
224 if let Some(template) = &self.node_template {
225 template.marker_ui(ui, adjusted_point.into(), self.zoom);
226 } else {
227 let mut shapes = Vec::new();
228 let color = if ui.visuals().dark_mode {
229 Color32::LIGHT_GREEN
230 } else {
231 Color32::GREEN
232 };
233 let millis = std::time::SystemTime::now()
234 .duration_since(std::time::UNIX_EPOCH)
235 .unwrap_or_default()
236 .as_millis();
237 let mut transparency = (millis % 2550 / 5) as i64;
238 if transparency > 255 {
239 transparency = 255 - (transparency - 255)
240 }
241 let corrected_color = Color32::from_rgba_unmultiplied(
242 color.r(),
243 color.g(),
244 color.b(),
245 transparency as u8,
246 );
247 shapes.push(Shape::Circle(CircleShape::stroke(
248 adjusted_point.into(),
249 4.0 * self.zoom,
250 Stroke::new(9.0 * self.zoom, corrected_color),
251 )));
252 ui.ctx().request_repaint();
253 ui.painter().extend(shapes);
254 }
255 }
256 }
257
258 self.paint_sub_components(ui, self.map_area);
259
260 self.capture_mouse_events(ui, &resp);
261
262 if self.zoom != self.previous_zoom {
263 profiling::scope!("calculating viewport with zoom");
264 self.adjust_bounds();
265 self.calculate_visible_points();
266 self.previous_zoom = self.zoom;
267 }
268
269 if let Some(menu_mon) = &mut self.menu_manager {
270 resp.context_menu(|ui| {
271 menu_mon.ui(ui);
272 });
273 }
274
275 #[cfg(feature = "debug_overlay")]
276 self.print_debug_info(paint, resp);
277 }
278 });
279 ui.allocate_space(self.map_area.size());
280 inner_response.response
281 }
282}
283
284impl Map {
285 pub fn new() -> Self {
290 let settings = MapSettings::default();
291 Self {
292 zoom: 1.0,
293 previous_zoom: 1.0,
294 map_area: Rect::NOTHING,
295 tree: None,
296 points: None,
297 labels: Vec::new(),
298 visible_points: Vec::new(),
299 current: MapBounds::default(),
300 reference: MapBounds::default(),
301 settings,
302 min_size: (None, None),
303 max_size: (None, None),
304 current_index: 0,
305 entities: HashMap::new(),
306 menu_manager: None,
307 node_template: None,
308 markers: HashMap::new(),
309 segments: None,
310 }
311 }
312
313 fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
314 let available = ui.available_rect_before_wrap();
315 let mut size = available.size();
316 if let Some(max_width) = self.max_size.0 {
317 size.x = size.x.min(max_width);
318 }
319 if let Some(max_height) = self.max_size.1 {
320 size.y = size.y.min(max_height);
321 }
322 if let Some(min_width) = self.min_size.0 {
323 size.x = size.x.max(min_width);
324 }
325 if let Some(min_height) = self.min_size.1 {
326 size.y = size.y.max(min_height);
327 }
328 self.map_area = Rect::from_min_size(available.min, size);
329 RawLine::new(
330 RawPoint::from(self.map_area.left_top()),
331 RawPoint::from(self.map_area.right_bottom()),
332 )
333 }
334
335 fn calculate_visible_points(&mut self) {
336 profiling::scope!("calculate_visible_points");
337 if self.current.dist > 0.0
338 && self.current.dist < f32::INFINITY
339 && let Some(tree) = &self.tree
340 {
341 let center = self.current.pos / self.zoom;
342 let radius = self.current.dist.powi(2);
343 let point: [f32; 2] = center.into();
344 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
345 self.visible_points.clear();
346 for point in vis_pos {
347 self.visible_points.push(point.1.cast_signed());
348 }
349 }
350 }
351
352 pub fn add_points(&mut self, points: Vec<MapPoint>) {
380 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
381 let mut hash_map = HashMap::new();
382 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
383 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
384 for entry in points {
385 for i in 0..min.components.len() {
386 if entry.coords[i] < min.components[i] {
387 min.components[i] = entry.coords[i];
388 }
389 if entry.coords[i] > max.components[i] {
390 max.components[i] = entry.coords[i];
391 }
392 }
393 let _result = tree.add(entry.coords, entry.get_id());
394 hash_map.insert(entry.get_id(), entry);
395 }
396 self.reference.min = min;
398 self.reference.max = max;
399 self.points = Some(hash_map);
400 self.tree = Some(tree);
401 self.reference.pos = RawLine::new(min, max).midpoint();
402 if self.map_area.area() == 0.0 {
406 self.reference.dist = 3000.00;
407 } else {
408 let rect = RawLine::new(
409 RawPoint::from(self.map_area.left_top()),
410 RawPoint::from(self.map_area.right_bottom()),
411 );
412 self.reference.dist = rect.distance();
413 }
414 self.current = self.reference.clone();
415 self.calculate_visible_points();
416 }
417
418 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
448 profiling::scope!("add_hashmap_points");
449 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
450 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
451 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
452
453 for entry in hash_map.iter() {
454 for i in 0..min.components.len() {
455 if entry.1.coords[i] < min.components[i] {
456 min.components[i] = entry.1.coords[i];
457 }
458 if entry.1.coords[i] > max.components[i] {
459 max.components[i] = entry.1.coords[i];
460 }
461 }
462 let _result = tree.add(entry.1.coords, *entry.0);
463 }
464
465 self.reference.min = min;
467 self.reference.max = max;
468 self.points = Some(hash_map);
469 self.tree = Some(tree);
470 self.reference.pos = RawLine::new(min, max).midpoint();
471 if self.map_area.area() == 0.0 {
475 self.reference.dist = 3000.00;
476 } else {
477 let rect = RawLine::new(
478 RawPoint::from(self.map_area.left_top()),
479 RawPoint::from(self.map_area.right_bottom()),
480 );
481 self.reference.dist = rect.distance();
482 }
483 self.current = self.reference.clone();
484 self.calculate_visible_points();
485 }
486
487 pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
492 profiling::scope!("set_pos_from_nodeid");
493 if let Some(hash_map) = &self.points
494 && let Some(map_point) = hash_map.get(&node_id)
495 {
496 self.reference.pos = RawPoint::from(map_point.coords);
497 self.adjust_bounds();
498 self.calculate_visible_points();
499 }
500 }
501
502 pub fn set_pos(&mut self, position: [f32; 2]) {
504 profiling::scope!("set_pos");
505 let point = RawPoint::from(position);
506 self.reference.pos = point;
507 self.adjust_bounds();
508 self.calculate_visible_points();
509 }
510
511 pub fn get_pos(&self) -> [f32; 2] {
513 profiling::scope!("get_pos");
514 self.reference.pos.into()
515 }
516
517 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
522 profiling::scope!("add_labels");
523 self.labels = labels;
524 }
525
526 pub fn add_lines(&mut self, segments: Vec<MapSegment>) {
538 profiling::scope!("add_lines");
539 self.segments = Some(rstar::RTree::bulk_load(segments));
544 }
545
546 pub fn add_hashmap_lines(&mut self, segments: HashMap<(usize, usize), MapSegment>) {
555 profiling::scope!("add_hashmap_lines");
556 let segments: Vec<MapSegment> = segments.into_values().collect();
557 self.segments = Some(rstar::RTree::bulk_load(segments));
558 }
559
560 fn adjust_bounds(&mut self) {
561 profiling::scope!("adjust_bounds");
562 self.current.max = self.reference.max * self.zoom;
563 self.current.min = self.reference.min * self.zoom;
564 self.current.dist = self.reference.dist / self.zoom;
565 self.current.pos = self.reference.pos * self.zoom;
566 }
567
568 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
569 profiling::scope!("capture_mouse_events");
570 if ui.rect_contains_pointer(self.map_area) {
572 ui.input(|x| {
573 profiling::scope!("capture_mouse_events");
574
575 if !x.events.is_empty() {
576 for event in &x.events {
577 match event {
578 Event::MouseWheel {
579 unit: _,
580 delta,
581 modifiers,
582 phase: _,
583 } => {
584 #[cfg(target_os = "macos")]
585 let zoom_modifier = if modifiers.mac_cmd {
586 delta.y / 80.00
587 } else {
588 delta.y / 400.00
589 };
590
591 #[cfg(not(target_os = "macos"))]
592 let zoom_modifier = if modifiers.ctrl {
593 delta.y / 8.00
594 } else {
595 delta.y / 40.00
596 };
597
598 let mut pre_zoom = self.zoom + zoom_modifier;
599 if pre_zoom > self.settings.max_zoom {
600 pre_zoom = self.settings.max_zoom;
601 }
602 if pre_zoom < self.settings.min_zoom {
603 pre_zoom = self.settings.min_zoom;
604 }
605 self.zoom = pre_zoom;
606 }
607 _ => {
608 continue;
609 }
610 };
611 }
612 }
613 });
614 }
615 }
616
617 pub fn set_zoom(&mut self, value: f32) {
622 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
623 self.zoom = value;
624 }
625 }
626
627 pub fn get_zoom(&mut self) -> f32 {
629 self.zoom
630 }
631
632 fn current_style(&self) -> &MapStyle {
635 self.settings
636 .styles
637 .get(self.current_index)
638 .or(self.settings.styles.first())
639 .expect("MapSettings::styles must not be empty")
640 }
641
642 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
643 let style_index = ui_obj.visuals().dark_mode as usize;
644
645 if self.current_index != style_index {
646 profiling::scope!("asign_visual_style");
647
648 self.current_index = style_index;
649 let map_style = self.settings.styles.get_mut(style_index).unwrap();
650 let visuals = &ui_obj.style().visuals;
651 map_style.background_color = visuals.extreme_bg_color;
652 map_style.border = Some(visuals.window_stroke);
653 }
654 }
655
656 #[cfg(feature = "debug_overlay")]
657 fn print_debug_info(&mut self, paint: Painter, resp: Response) {
658 profiling::scope!("printing debug data");
659
660 let mut init_pos = Pos2::new(
661 self.map_area.left_top().x + 10.00,
662 self.map_area.left_top().y + 10.00,
663 );
664 let mut msg = "MIN:".to_string()
665 + self.current.min.components[0].to_string().as_str()
666 + ","
667 + self.current.min.components[1].to_string().as_str();
668 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
669 init_pos.y += 15.0;
670 msg = "MAX:".to_string()
671 + self.current.max.components[0].to_string().as_str()
672 + ","
673 + self.current.max.components[1].to_string().as_str();
674 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
675 init_pos.y += 15.0;
676 msg = "CUR:(".to_string()
677 + self.current.pos.components[0].to_string().as_str()
678 + ","
679 + self.current.pos.components[1].to_string().as_str()
680 + ")";
681 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
682 init_pos.y += 15.0;
683 msg = "DST:".to_string() + self.current.dist.to_string().as_str();
684 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
685 init_pos.y += 15.0;
686 msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
687 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
688 init_pos.y += 15.0;
689 msg = "REC:(".to_string()
690 + self.map_area.left_top().x.to_string().as_str()
691 + ","
692 + self.map_area.left_top().y.to_string().as_str()
693 + "),("
694 + self.map_area.right_bottom().x.to_string().as_str()
695 + ","
696 + self.map_area.right_bottom().y.to_string().as_str()
697 + ")";
698 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
699 if let Some(points) = &self.points {
700 init_pos.y += 15.0;
701 msg = "NUM:".to_string() + points.len().to_string().as_str();
702 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
703 }
704 if !self.visible_points.is_empty() {
705 init_pos.y += 15.0;
706 msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
707 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
708 }
709 if let Some(pointer_pos) = resp.hover_pos() {
710 init_pos.y += 15.0;
711 msg = "HVR:".to_string()
712 + pointer_pos.x.to_string().as_str()
713 + ","
714 + pointer_pos.y.to_string().as_str();
715 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
716 }
717 let vec = resp.drag_delta();
718 if vec.length() != 0.0 {
719 init_pos.y += 15.0;
720 msg = "DRG:".to_string()
721 + vec.to_pos2().x.to_string().as_str()
722 + ","
723 + vec.to_pos2().y.to_string().as_str();
724 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
725 }
726 }
727
728 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
729 profiling::scope!("map_ui_paint_sub_components");
730 let zoom_slider = egui::Slider::new(
731 &mut self.zoom,
732 self.settings.min_zoom..=self.settings.max_zoom,
733 )
734 .show_value(false)
735 .orientation(SliderOrientation::Vertical);
736 let mut pos1 = rect.right_top();
737 let mut pos2 = rect.right_top();
738 pos1.x -= 80.0;
739 pos1.y += 120.0;
740 pos2.x -= 60.0;
741 pos2.y += 240.0;
742
743 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
744 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
745 ui_obj.scope_builder(ui_builder, |ui_obj| {
746 ui_obj.add(zoom_slider);
747 });
748 }
749
750 fn paint_map_points(
751 &self,
752 vec_points: &Vec<isize>,
753 hashm: &Option<HashMap<usize, MapPoint>>,
754 paint: &Painter,
755 ui_obj: &mut Ui,
756 min_point: &RawPoint,
757 resp: &Response,
758 ) -> Result<Vec<usize>, ()> {
759 let mut nearest_id = None;
760 let mut nodes_to_remove = Vec::new();
761 let mut shape_vec = vec![];
762
763 if hashm.is_none() {
764 return Err(());
765 }
766 if vec_points.is_empty() {
767 return Err(());
768 }
769 if self.settings.node_text_visibility == VisibilitySetting::Hover
771 && resp.hovered()
772 && let Some(point) = resp.hover_pos()
773 {
774 let raw_point = RawPoint::from(point);
775 let hovered_map_point = (*min_point + raw_point) / self.zoom;
776 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
777 &hovered_map_point.components,
778 1,
779 &squared_euclidean,
780 ) {
781 nearest_id = Some(nearest_node.first().unwrap().1);
782 }
783 }
784 let mut text_settings = TextSettings {
786 size: 12.00 * self.zoom,
787 anchor: Align2::LEFT_BOTTOM,
788 family: FontFamily::Proportional,
789 text: String::new(),
790 position: RawPoint::default(),
791 text_color: ui_obj.visuals().text_color(),
792 };
793
794 for temp_point in vec_points {
796 let parsed_point = temp_point.cast_unsigned();
797 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
798 profiling::scope!("painting_points_m");
799 let viewport_point = RawPoint::from(system.coords) * self.zoom - min_point;
800 if let Some(node_template) = &self.node_template {
801 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
802 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
803 }
804 } else if self.zoom > self.settings.label_visible_zoom
805 && self.settings.node_text_visibility == VisibilitySetting::Always
806 || (self.settings.node_text_visibility == VisibilitySetting::Hover
807 && nearest_id.unwrap_or(&0usize) == &system.get_id())
808 {
809 let mut viewport_text = viewport_point;
810 viewport_text.components[0] += 3.0 * self.zoom;
811 viewport_text.components[1] -= 3.0 * self.zoom;
812 text_settings.position = viewport_text;
813 text_settings.text = system.get_name();
814 self.paint_label(paint, &text_settings);
815 }
816
817 let system_id = system.get_id();
818 if let Some(init_time) = self.entities.get(&system_id) {
819 if let Some(template) = &self.node_template {
820 template.notification_ui(
821 ui_obj,
822 viewport_point.into(),
823 self.zoom,
824 *init_time,
825 self.current_style().alert_color,
826 );
827 } else if Animation::pulse(
828 paint,
829 viewport_point,
830 self.zoom,
831 *init_time,
832 self.current_style().alert_color,
833 ) {
834 ui_obj.ctx().request_repaint();
835 } else {
836 nodes_to_remove.push(system_id);
837 }
838 }
839 if let Some(node_template) = &self.node_template {
840 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
841 } else {
842 shape_vec.push(Shape::circle_filled(
843 viewport_point.into(),
844 4.00 * self.zoom,
845 self.current_style().fill_color,
846 ));
847 }
848 }
849 }
850 paint.extend(shape_vec);
851 Ok(nodes_to_remove)
852 }
853
854 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
855 profiling::scope!("paint_map_lines");
856
857 if self.zoom > self.settings.line_visible_zoom
859 && let Some(mut stroke) = self.current_style().line
860 && let Some(segments) = &self.segments
861 {
862 let mut shape_vec = vec![];
863 let transparency_range = self.zoom - self.settings.line_visible_zoom;
864 if (0.00..0.80).contains(&transparency_range) {
865 let mut tup_stroke = stroke.color.to_tuple();
866 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
867 tup_stroke.3 = (255.0 * transparency).round() as u8;
868 let color = Color32::from_rgba_unmultiplied(
869 tup_stroke.0,
870 tup_stroke.1,
871 tup_stroke.2,
872 tup_stroke.3,
873 );
874 stroke = Stroke::new(stroke.width, color);
875 }
876 let center = self.current.pos / self.zoom;
880 let padding = stroke.width / self.zoom;
881 let half = RawPoint::new(
882 self.map_area.width() / 2.0 / self.zoom + padding,
883 self.map_area.height() / 2.0 / self.zoom + padding,
884 );
885 let query = rstar::AABB::from_corners((center - half).into(), (center + half).into());
886 for segment in segments.locate_in_envelope_intersecting(query) {
887 let raw_line = segment.raw_line();
888 let pos_a = raw_line.points[0] * self.zoom - min_point;
889 let pos_b = raw_line.points[1] * self.zoom - min_point;
890 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
891 }
892 painter.extend(shape_vec);
893 }
894 }
895
896 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
897 profiling::scope!("paint_label");
898 paint.text(
899 text_settings.position.into(),
900 text_settings.anchor,
901 text_settings.text.clone(),
902 FontId::new(text_settings.size, text_settings.family.clone()),
903 text_settings.text_color,
904 );
905 }
906
907 pub fn notify(&mut self, id_node: usize, time: Instant) {
914 profiling::scope!("notify");
915 self.entities
916 .entry(id_node)
917 .and_modify(|value| *value = time)
918 .or_insert(time);
919 }
920
921 pub fn line_at(&self, point: [f32; 2], tolerance: f32) -> Option<(usize, usize)> {
935 profiling::scope!("line_at");
936 let segments = self.segments.as_ref()?;
937 let tolerance = tolerance.max(0.0);
938
939 let center = RawPoint::from(point);
940 let padding = RawPoint::new(tolerance, tolerance);
941 let query = rstar::AABB::from_corners((center - padding).into(), (center + padding).into());
942
943 let mut closest: Option<(f32, (usize, usize))> = None;
944 for segment in segments.locate_in_envelope_intersecting(query) {
945 let distance = segment.raw_line().distance_to_point(center);
946 if distance <= tolerance && closest.as_ref().is_none_or(|(best, _)| distance < *best) {
947 closest = Some((distance, segment.id));
948 }
949 }
950 closest.map(|(_, id)| id)
951 }
952
953 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
956 self.menu_manager = Some(manager);
957 }
958
959 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
967 self.node_template = Some(template);
968 }
969
970 pub fn update_marker(&mut self, id: usize, node_id: usize) {
975 self.markers
976 .entry(id)
977 .and_modify(|value| *value = node_id)
978 .or_insert(node_id);
979 }
980
981 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
984 self.min_size = (width, height);
985 }
986
987 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
990 self.max_size = (width, height);
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use std::time::Duration;
998
999 fn sample_points() -> Vec<MapPoint> {
1000 let mut map = Vec::new();
1001 map.push(MapPoint::new(1, [0.0, 0.0]));
1002 map.push(MapPoint::new(2, [10.0, 10.0]));
1003 map.push(MapPoint::new(3, [-10.0, -10.0]));
1004 map
1005 }
1006
1007 #[test]
1010 fn map_new_initial_state() {
1011 let map = Map::new();
1012 assert_eq!(map.zoom, 1.0);
1013 assert_eq!(map.previous_zoom, 1.0);
1014 assert!(map.points.is_none());
1015 assert!(map.segments.is_none());
1016 assert!(map.tree.is_none());
1017 assert!(map.labels.is_empty());
1018 assert!(map.visible_points.is_empty());
1019 assert!(map.markers.is_empty());
1020 assert!(map.entities.is_empty());
1021 assert_eq!(map.min_size, (None, None));
1022 assert_eq!(map.max_size, (None, None));
1023 assert_eq!(map.current_index, 0);
1024 }
1025
1026 #[test]
1027 fn map_default_equals_new() {
1028 let map = Map::default();
1029 assert_eq!(map.zoom, 1.0);
1030 assert!(map.points.is_none());
1031 }
1032
1033 #[test]
1036 fn set_zoom_within_range() {
1037 let mut map = Map::new();
1038 map.set_zoom(1.5);
1039 assert_eq!(map.get_zoom(), 1.5);
1040 }
1041
1042 #[test]
1043 fn set_zoom_at_exact_limits() {
1044 let mut map = Map::new();
1045 map.set_zoom(map.settings.min_zoom);
1046 assert_eq!(map.get_zoom(), 0.1);
1047 map.set_zoom(map.settings.max_zoom);
1048 assert_eq!(map.get_zoom(), 2.0);
1049 }
1050
1051 #[test]
1052 fn set_zoom_out_of_range_is_ignored() {
1053 let mut map = Map::new();
1054 let initial = map.get_zoom();
1055 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
1057 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
1059 }
1060
1061 #[test]
1064 fn add_hashmap_points_computes_bounds() {
1065 let mut map = Map::new();
1066 map.add_points(sample_points());
1067
1068 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
1069 assert_eq!(map.reference.max.components, [10.0, 10.0]);
1070 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1072 assert_eq!(map.reference.dist, 3000.0);
1074 assert_eq!(map.current.min.components, map.reference.min.components);
1076 assert_eq!(map.current.max.components, map.reference.max.components);
1077 assert_eq!(map.current.pos.components, map.reference.pos.components);
1078 assert_eq!(map.current.dist, map.reference.dist);
1079 assert!(map.points.is_some());
1080 assert!(map.tree.is_some());
1081 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1082 }
1083
1084 #[test]
1085 fn add_hashmap_points_populates_visible_points() {
1086 let mut map = Map::new();
1087 map.add_points(sample_points());
1088 assert_eq!(map.visible_points.len(), 3);
1090 }
1091
1092 fn render_line_segments(map: &mut Map) -> Vec<[egui::Pos2; 2]> {
1095 use egui::{Context, RawInput, Shape};
1096 let ctx = Context::default();
1097 let input = RawInput {
1098 screen_rect: Some(egui::Rect::from_min_size(
1099 egui::Pos2::ZERO,
1100 egui::vec2(500.0, 500.0),
1101 )),
1102 ..RawInput::default()
1103 };
1104 let output = ctx.run_ui(input, |ui| {
1105 ui.add(&mut *map);
1106 });
1107 output
1108 .shapes
1109 .iter()
1110 .filter_map(|cs| match cs.shape {
1111 Shape::LineSegment { points, .. } => Some(points),
1112 _ => None,
1113 })
1114 .collect()
1115 }
1116
1117 #[test]
1118 fn segment_crossing_viewport_is_painted_even_with_far_endpoints() {
1119 let mut map = Map::new();
1124 map.set_zoom(1.0);
1125 let mut lines = Vec::new();
1126 lines.push(MapSegment::new((1, 2), [-4000.0, -1.0], [4000.0, 1.0]));
1127 map.add_lines(lines);
1128 map.set_pos([0.0, 0.0]);
1129
1130 let segments = render_line_segments(&mut map);
1131 assert_eq!(segments.len(), 1);
1132 }
1133
1134 #[test]
1135 fn segment_outside_viewport_is_not_painted() {
1136 let mut map = Map::new();
1137 map.set_zoom(1.0);
1138 let mut lines = Vec::new();
1139 lines.push(MapSegment::new(
1140 (1, 2),
1141 [10_000.0, 10_000.0],
1142 [10_100.0, 10_100.0],
1143 ));
1144 map.add_lines(lines);
1145 map.set_pos([0.0, 0.0]);
1146
1147 assert!(render_line_segments(&mut map).is_empty());
1148 }
1149
1150 #[test]
1151 fn add_lines_builds_segment_tree() {
1152 let mut map = Map::new();
1153 map.add_points(sample_points());
1154 let mut lines = Vec::new();
1155 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1156 map.add_lines(lines);
1157
1158 let tree = map
1159 .segments
1160 .as_ref()
1161 .expect("add_lines must build the segment tree");
1162 assert_eq!(tree.size(), 1);
1163
1164 let hit_query = rstar::AABB::from_corners([-1.0, -1.0], [1.0, 1.0]);
1167 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1168 assert_eq!(hits.len(), 1);
1169 assert_eq!(hits[0].id, (1, 2));
1170
1171 let miss_query = rstar::AABB::from_corners([100.0, 100.0], [200.0, 200.0]);
1172 assert_eq!(tree.locate_in_envelope_intersecting(miss_query).count(), 0);
1173 }
1174
1175 #[test]
1176 fn map_check_line_is_painted_on_first_frame() {
1177 use egui::{Context, RawInput, Shape};
1178
1179 let mut map = Map::new();
1181 map.set_zoom(1.0);
1182
1183 let mut point_a = MapPoint::new(0, [0.0, 0.0]);
1184 point_a.connections.push((0, 1));
1185 let mut point_b = MapPoint::new(1, [50.0, 50.0]);
1186 point_b.connections.push((0, 1));
1187
1188 let mut lines = Vec::new();
1189 lines.push(MapSegment::new((0, 1), point_a.coords, point_b.coords));
1190
1191 let mut points = Vec::new();
1192 points.push(point_a);
1193 points.push(point_b);
1194 map.add_points(points);
1196 map.add_lines(lines);
1197
1198 map.set_pos([25.0, 25.0]);
1199
1200 let ctx = Context::default();
1202 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1203 let input = RawInput {
1204 screen_rect: Some(screen),
1205 ..RawInput::default()
1206 };
1207
1208 let output1 = ctx.run_ui(input.clone(), |ui| {
1209 ui.add(&mut map);
1210 });
1211
1212 let segments1: Vec<[egui::Pos2; 2]> = output1
1213 .shapes
1214 .iter()
1215 .filter_map(|cs| match cs.shape {
1216 Shape::LineSegment { points, .. } => Some(points),
1217 _ => None,
1218 })
1219 .collect();
1220
1221 assert!(
1222 !segments1.is_empty(),
1223 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
1224 );
1225
1226 let expected_a = egui::pos2(225.0, 225.0);
1229 let expected_b = egui::pos2(275.0, 275.0);
1230 let tolerance = 2.0;
1231 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
1232 let d_a1 = p1.distance(expected_a);
1233 let d_b1 = p2.distance(expected_b);
1234 let d_a2 = p2.distance(expected_a);
1235 let d_b2 = p1.distance(expected_b);
1236 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
1237 });
1238 assert!(
1239 found_on_frame1,
1240 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
1241 segments1
1242 );
1243
1244 let output2 = ctx.run_ui(input, |ui| {
1246 ui.add(&mut map);
1247 });
1248
1249 let segments2: Vec<[egui::Pos2; 2]> = output2
1250 .shapes
1251 .iter()
1252 .filter_map(|cs| match cs.shape {
1253 Shape::LineSegment { points, .. } => Some(points),
1254 _ => None,
1255 })
1256 .collect();
1257
1258 assert_eq!(
1259 segments1.len(),
1260 segments2.len(),
1261 "Frame 2: expected {} line segments (no duplication across frames), got {}",
1262 segments1.len(),
1263 segments2.len()
1264 );
1265 }
1266
1267 #[test]
1270 fn set_pos_and_get_pos_roundtrip() {
1271 let mut map = Map::new();
1272 map.set_pos([25.0, -35.0]);
1273 assert_eq!(map.get_pos(), [25.0, -35.0]);
1274 }
1275
1276 #[test]
1277 fn set_pos_from_nodeid_with_valid_id() {
1278 let mut map = Map::new();
1279 map.add_points(sample_points());
1280 map.set_pos_from_nodeid(2);
1281 assert_eq!(map.get_pos(), [10.0, 10.0]);
1282 }
1283
1284 #[test]
1285 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1286 let mut map = Map::new();
1287 map.add_points(sample_points());
1288 let before = map.reference.pos.components;
1289 map.set_pos_from_nodeid(999);
1290 assert_eq!(map.reference.pos.components, before);
1291 }
1292
1293 #[test]
1294 fn set_pos_from_nodeid_without_points_does_nothing() {
1295 let mut map = Map::new();
1296 map.set_pos_from_nodeid(1);
1297 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1298 }
1299
1300 #[test]
1303 fn add_labels_stores_labels() {
1304 let mut map = Map::new();
1305 let label = MapLabel {
1306 text: "Region".to_string(),
1307 center: Pos2::new(1.0, 2.0),
1308 };
1309 map.add_labels(vec![label]);
1310 assert_eq!(map.labels.len(), 1);
1311 assert_eq!(map.labels[0].text, "Region");
1312 }
1313
1314 #[test]
1315 fn add_lines_stores_lines() {
1316 let mut map = Map::new();
1317 let mut lines = Vec::new();
1318 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [1.0, 1.0]));
1319 map.add_lines(lines);
1320 let tree = map.segments.as_ref().unwrap();
1321 assert_eq!(tree.size(), 1);
1322 assert_eq!(
1323 tree.locate_in_envelope_intersecting(rstar::AABB::from_corners(
1324 [-1.0, -1.0],
1325 [2.0, 2.0],
1326 ))
1327 .next()
1328 .unwrap()
1329 .id,
1330 (1, 2)
1331 );
1332 }
1333
1334 #[test]
1337 fn line_at_returns_closest_line_within_tolerance() {
1338 let mut map = Map::new();
1339 map.add_points(sample_points());
1340 let mut lines = Vec::new();
1341 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]));
1342 lines.push(MapSegment::new((3, 4), [20.0, -5.0], [20.0, 5.0]));
1343 map.add_lines(lines);
1344
1345 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
1347 assert_eq!(hit, (1, 2));
1348
1349 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
1351 assert_eq!(hit, (3, 4));
1352 }
1353
1354 #[test]
1355 fn line_at_returns_none_beyond_tolerance() {
1356 let mut map = Map::new();
1357 map.add_points(sample_points());
1358 let mut lines = Vec::new();
1359 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1360 map.add_lines(lines);
1361
1362 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
1365 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
1366 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
1367 }
1368
1369 #[test]
1370 fn line_at_returns_none_without_lines() {
1371 let map = Map::new();
1372 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
1373 }
1374
1375 #[test]
1376 fn line_at_negative_tolerance_behaves_like_zero() {
1377 let mut map = Map::new();
1378 map.add_points(sample_points());
1379 let mut lines = Vec::new();
1380 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1381 map.add_lines(lines);
1382
1383 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
1385 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
1386 }
1387
1388 #[test]
1389 fn notify_inserts_and_updates_entities() {
1390 let mut map = Map::new();
1391 let t1 = Instant::now();
1392 map.notify(5, t1);
1393 assert_eq!(map.entities.get(&5), Some(&t1));
1394
1395 let t2 = t1 + Duration::from_secs(1);
1396 map.notify(5, t2);
1397 assert_eq!(map.entities.get(&5), Some(&t2));
1398 assert_eq!(map.entities.len(), 1);
1399 }
1400
1401 #[test]
1402 fn update_marker_inserts_and_updates() {
1403 let mut map = Map::new();
1404 map.update_marker(1, 100);
1405 assert_eq!(map.markers.get(&1), Some(&100));
1406 map.update_marker(1, 200);
1407 assert_eq!(map.markers.get(&1), Some(&200));
1408 assert_eq!(map.markers.len(), 1);
1409 }
1410
1411 #[test]
1414 fn allocate_at_least_sets_min_size() {
1415 let mut map = Map::new();
1416 map.allocate_at_least(Some(100.0), None);
1417 assert_eq!(map.min_size, (Some(100.0), None));
1418 }
1419
1420 #[test]
1421 fn allocate_at_most_sets_max_size() {
1422 let mut map = Map::new();
1423 map.allocate_at_most(None, Some(200.0));
1424 assert_eq!(map.max_size, (None, Some(200.0)));
1425 }
1426
1427 #[test]
1430 fn adjust_bounds_scales_with_zoom() {
1431 let mut map = Map::new();
1432 map.reference.min = RawPoint::new(-10.0, -20.0);
1433 map.reference.max = RawPoint::new(10.0, 20.0);
1434 map.reference.pos = RawPoint::new(5.0, 5.0);
1435 map.reference.dist = 100.0;
1436 map.set_zoom(2.0);
1437 map.adjust_bounds();
1438
1439 assert_eq!(map.current.max.components, [20.0, 40.0]);
1440 assert_eq!(map.current.min.components, [-20.0, -40.0]);
1441 assert_eq!(map.current.pos.components, [10.0, 10.0]);
1442 assert_eq!(map.current.dist, 50.0);
1443 }
1444}