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