1use crate::map::animation::Animation;
78use crate::map::objects::{
79 ContextMenuManager, MapBounds, MapLabel, MapPoint, MapSegment, MapSettings, MapStyle, RawLine,
80 RawPoint, TextSettings, VisibilitySetting,
81};
82use egui::{epaint::CircleShape, widgets::*, *};
83use kdtree::KdTree;
84use kdtree::distance::squared_euclidean;
85use std::collections::{HashMap};
86use std::rc::Rc;
87use std::time::Instant;
88
89use self::objects::NodeTemplate;
90
91pub mod animation;
92pub mod objects;
93
94#[derive(Clone)]
132pub struct Map {
133 zoom: f32,
134 previous_zoom: f32,
135 points: Option<HashMap<usize, MapPoint>>,
136 segments: Option<rstar::RTree<MapSegment>>,
137 labels: Vec<MapLabel>,
138 tree: Option<KdTree<f32, usize, [f32; 2]>>,
139 visible_points: Vec<isize>,
140 map_area: Rect,
141 reference: MapBounds,
142 current: MapBounds,
143 current_index: usize,
144 entities: HashMap<usize, Instant>,
145 min_size: (Option<f32>, Option<f32>),
146 max_size: (Option<f32>, Option<f32>),
147 pub settings: MapSettings,
150 menu_manager: Option<Rc<dyn ContextMenuManager>>,
151 node_template: Option<Rc<dyn NodeTemplate>>,
152 markers: HashMap<usize, usize>,
153}
154
155impl Default for Map {
156 fn default() -> Self {
158 Map::new()
159 }
160}
161
162impl Widget for &mut Map {
163 fn ui(self, ui: &mut egui::Ui) -> Response {
166 let rect = self.calculate_widget_dimensions(ui);
167
168 self.reference.dist = rect.distance();
170
171 self.assign_visual_style(ui);
172
173 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
174
175 let inner_response = canvas.show(ui, |ui| {
176 #[cfg(feature = "puffin")]
177 puffin::profile_scope!("paint_map");
178
179 if ui.is_rect_visible(self.map_area) {
180 let (resp, paint) =
181 ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
182 let vec = resp.drag_delta();
183 if vec.length() != 0.0 {
184 #[cfg(feature = "puffin")]
185 puffin::profile_scope!("calculating_points_in_visible_area");
186
187 let coords = RawPoint::from(vec.to_pos2());
188 let new_pos = self.reference.pos - (coords / self.zoom);
189 self.set_pos(new_pos.into());
190 }
191 if self.zoom < self.settings.line_visible_zoom {
192 let mut text_settings = TextSettings {
194 size: 12.00 * self.zoom * 2.00,
195 anchor: Align2::CENTER_CENTER,
196 family: FontFamily::Proportional,
197 text: String::new(),
198 position: RawPoint::default(),
199 text_color: ui.visuals().text_color(),
200 };
201 for label in &self.labels {
202 text_settings.text.clone_from(&label.text);
203 text_settings.position = RawPoint::from(label.center);
204 self.paint_label(&paint, &text_settings);
205 }
206 }
207
208 let rect_midpoint = RawPoint::from(self.map_area.center());
209 let min_point = self.current.pos - rect_midpoint;
210 let vec_points = &self.visible_points;
211 let hashm = &self.points;
212
213 let now = Instant::now();
216 self.entities
217 .retain(|_, init| now.duration_since(*init).as_secs_f32() < 10.0);
218
219 self.paint_map_lines(&paint, &min_point);
220
221 if let Ok(nodes_to_remove) =
222 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
223 {
224 for node in nodes_to_remove {
225 self.entities.remove(&node);
226 }
227 }
228
229 for marker in &self.markers {
230 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
231 let adjusted_point = point.raw_point * self.zoom - min_point;
232 if let Some(template) = &self.node_template {
233 template.marker_ui(ui, adjusted_point.into(), self.zoom);
234 } else {
235 let mut shapes = Vec::new();
236 let color = if ui.visuals().dark_mode {
237 Color32::LIGHT_GREEN
238 } else {
239 Color32::GREEN
240 };
241 let millis = std::time::SystemTime::now()
242 .duration_since(std::time::UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_millis();
245 let mut transparency = (millis % 2550 / 5) as i64;
246 if transparency > 255 {
247 transparency = 255 - (transparency - 255)
248 }
249 let corrected_color = Color32::from_rgba_unmultiplied(
250 color.r(),
251 color.g(),
252 color.b(),
253 transparency as u8,
254 );
255 shapes.push(Shape::Circle(CircleShape::stroke(
256 adjusted_point.into(),
257 4.0 * self.zoom,
258 Stroke::new(9.0 * self.zoom, corrected_color),
259 )));
260 ui.ctx().request_repaint();
261 ui.painter().extend(shapes);
262 }
263 }
264 }
265
266 self.paint_sub_components(ui, self.map_area);
267
268 self.capture_mouse_events(ui, &resp);
269
270 if self.zoom != self.previous_zoom {
271 #[cfg(feature = "puffin")]
272 puffin::profile_scope!("calculating viewport with zoom");
273 self.adjust_bounds();
274 self.calculate_visible_points();
275 self.previous_zoom = self.zoom;
276 }
277
278 if let Some(menu_mon) = &mut self.menu_manager {
279 resp.context_menu(|ui| {
280 menu_mon.ui(ui);
281 });
282 }
283
284 #[cfg(feature = "debug_overlay")]
285 self.print_debug_info(paint, resp);
286 }
287 });
288 ui.allocate_space(self.map_area.size());
289 inner_response.response
290 }
291}
292
293impl Map {
294 pub fn new() -> Self {
299 let settings = MapSettings::default();
300 Self {
301 zoom: 1.0,
302 previous_zoom: 1.0,
303 map_area: Rect::NOTHING,
304 tree: None,
305 points: 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 markers: HashMap::new(),
318 segments: None,
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 for point in vis_pos {
357 self.visible_points.push(point.1.cast_signed());
358 }
359 }
360 }
361
362pub fn add_points(&mut self,points: Vec<MapPoint>) {
390 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
391 let mut hash_map = HashMap::new();
392 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
393 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
394 for entry in points {
395 for i in 0..min.components.len() {
396 if entry.raw_point.components[i] < min.components[i] {
397 min.components[i] = entry.raw_point.components[i];
398 }
399 if entry.raw_point.components[i] > max.components[i] {
400 max.components[i] = entry.raw_point.components[i];
401 }
402 }
403 let _result = tree.add(entry.raw_point.components, entry.get_id());
404 hash_map.insert(entry.get_id(), entry);
405 }
406 self.reference.min = min;
408 self.reference.max = max;
409 self.points = Some(hash_map);
410 self.tree = Some(tree);
411 self.reference.pos = RawLine::new(min, max).midpoint();
412 if self.map_area.area() == 0.0 {
416 self.reference.dist = 3000.00;
417 } else {
418 let rect = RawLine::new(
419 RawPoint::from(self.map_area.left_top()),
420 RawPoint::from(self.map_area.right_bottom()),
421 );
422 self.reference.dist = rect.distance();
423 }
424 self.current = self.reference.clone();
425 self.calculate_visible_points();
426 }
427
428 #[deprecated(since="0.2.3", note="please use `add_points` instead")]
457 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
458 #[cfg(feature = "puffin")]
459 puffin::profile_scope!("add_hashmap_points");
460 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
461 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
462 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
463
464 for entry in hash_map.iter() {
465 for i in 0..min.components.len() {
466 if entry.1.raw_point.components[i] < min.components[i] {
467 min.components[i] = entry.1.raw_point.components[i];
468 }
469 if entry.1.raw_point.components[i] > max.components[i] {
470 max.components[i] = entry.1.raw_point.components[i];
471 }
472 }
473 let _result = tree.add(entry.1.raw_point.into(), *entry.0);
474 }
475
476 self.reference.min = min;
478 self.reference.max = max;
479 self.points = Some(hash_map);
480 self.tree = Some(tree);
481 self.reference.pos = RawLine::new(min, max).midpoint();
482 if self.map_area.area() == 0.0 {
486 self.reference.dist = 3000.00;
487 } else {
488 let rect = RawLine::new(
489 RawPoint::from(self.map_area.left_top()),
490 RawPoint::from(self.map_area.right_bottom()),
491 );
492 self.reference.dist = rect.distance();
493 }
494 self.current = self.reference.clone();
495 self.calculate_visible_points();
496 }
497
498 pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
503 #[cfg(feature = "puffin")]
504 puffin::profile_scope!("set_pos_from_nodeid");
505 if let Some(hash_map) = &self.points
506 && let Some(map_point) = hash_map.get(&node_id)
507 {
508 self.reference.pos = map_point.raw_point;
509 self.adjust_bounds();
510 self.calculate_visible_points();
511 }
512 }
513
514 pub fn set_pos(&mut self, position: [f32; 2]) {
516 #[cfg(feature = "puffin")]
517 puffin::profile_scope!("set_pos");
518 let point = RawPoint::from(position);
519 self.reference.pos = point;
520 self.adjust_bounds();
521 self.calculate_visible_points();
522 }
523
524 pub fn get_pos(&self) -> [f32; 2] {
526 #[cfg(feature = "puffin")]
527 puffin::profile_scope!("get_pos");
528 self.reference.pos.into()
529 }
530
531 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
536 #[cfg(feature = "puffin")]
537 puffin::profile_scope!("add_labels");
538 self.labels = labels;
539 }
540
541 pub fn add_lines(&mut self, segments: Vec<MapSegment>) {
553 #[cfg(feature = "puffin")]
554 puffin::profile_scope!("add_lines");
555 self.segments = Some(rstar::RTree::bulk_load(segments));
560 }
561
562 fn adjust_bounds(&mut self) {
563 #[cfg(feature = "puffin")]
564 puffin::profile_scope!("adjust_bounds");
565 self.current.max = self.reference.max * self.zoom;
566 self.current.min = self.reference.min * self.zoom;
567 self.current.dist = self.reference.dist / self.zoom;
568 self.current.pos = self.reference.pos * self.zoom;
569 }
570
571 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
572 #[cfg(feature = "puffin")]
573 puffin::profile_scope!("capture_mouse_events");
574 if ui.rect_contains_pointer(self.map_area) {
576 ui.input(|x| {
577 #[cfg(feature = "puffin")]
578 puffin::profile_scope!("capture_mouse_events");
579
580 if !x.events.is_empty() {
581 for event in &x.events {
582 match event {
583 Event::MouseWheel {
584 unit: _,
585 delta,
586 modifiers,
587 phase: _,
588 } => {
589 #[cfg(target_os = "macos")]
590 let zoom_modifier = if modifiers.mac_cmd {
591 delta.y / 80.00
592 } else {
593 delta.y / 400.00
594 };
595
596 #[cfg(not(target_os = "macos"))]
597 let zoom_modifier = if modifiers.ctrl {
598 delta.y / 8.00
599 } else {
600 delta.y / 40.00
601 };
602
603 let mut pre_zoom = self.zoom + zoom_modifier;
604 if pre_zoom > self.settings.max_zoom {
605 pre_zoom = self.settings.max_zoom;
606 }
607 if pre_zoom < self.settings.min_zoom {
608 pre_zoom = self.settings.min_zoom;
609 }
610 self.zoom = pre_zoom;
611 }
612 _ => {
613 continue;
614 }
615 };
616 }
617 }
618 });
619 }
620 }
621
622 pub fn set_zoom(&mut self, value: f32) {
627 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
628 self.zoom = value;
629 }
630 }
631
632 pub fn get_zoom(&mut self) -> f32 {
634 self.zoom
635 }
636
637 fn current_style(&self) -> &MapStyle {
640 self.settings
641 .styles
642 .get(self.current_index)
643 .or(self.settings.styles.first())
644 .expect("MapSettings::styles must not be empty")
645 }
646
647 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
648 let style_index = ui_obj.visuals().dark_mode as usize;
649
650 if self.current_index != style_index {
651 #[cfg(feature = "puffin")]
652 puffin::profile_scope!("asign_visual_style");
653
654 self.current_index = style_index;
655 let map_style = self.settings.styles.get_mut(style_index).unwrap();
656 let visuals = &ui_obj.style().visuals;
657 map_style.background_color = visuals.extreme_bg_color;
658 map_style.border = Some(visuals.window_stroke);
659 }
660 }
661
662 #[cfg(feature = "debug_overlay")]
663 fn print_debug_info(&mut self, paint: Painter, resp: Response) {
664 #[cfg(feature = "puffin")]
665 puffin::profile_scope!("printing debug data");
666
667 let mut init_pos = Pos2::new(
668 self.map_area.left_top().x + 10.00,
669 self.map_area.left_top().y + 10.00,
670 );
671 let mut msg = "MIN:".to_string()
672 + self.current.min.components[0].to_string().as_str()
673 + ","
674 + self.current.min.components[1].to_string().as_str();
675 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
676 init_pos.y += 15.0;
677 msg = "MAX:".to_string()
678 + self.current.max.components[0].to_string().as_str()
679 + ","
680 + self.current.max.components[1].to_string().as_str();
681 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
682 init_pos.y += 15.0;
683 msg = "CUR:(".to_string()
684 + self.current.pos.components[0].to_string().as_str()
685 + ","
686 + self.current.pos.components[1].to_string().as_str()
687 + ")";
688 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
689 init_pos.y += 15.0;
690 msg = "DST:".to_string() + self.current.dist.to_string().as_str();
691 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
692 init_pos.y += 15.0;
693 msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
694 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
695 init_pos.y += 15.0;
696 msg = "REC:(".to_string()
697 + self.map_area.left_top().x.to_string().as_str()
698 + ","
699 + self.map_area.left_top().y.to_string().as_str()
700 + "),("
701 + self.map_area.right_bottom().x.to_string().as_str()
702 + ","
703 + self.map_area.right_bottom().y.to_string().as_str()
704 + ")";
705 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
706 if let Some(points) = &self.points {
707 init_pos.y += 15.0;
708 msg = "NUM:".to_string() + points.len().to_string().as_str();
709 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
710 }
711 if !self.visible_points.is_empty() {
712 init_pos.y += 15.0;
713 msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
714 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
715 }
716 if let Some(pointer_pos) = resp.hover_pos() {
717 init_pos.y += 15.0;
718 msg = "HVR:".to_string()
719 + pointer_pos.x.to_string().as_str()
720 + ","
721 + pointer_pos.y.to_string().as_str();
722 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
723 }
724 let vec = resp.drag_delta();
725 if vec.length() != 0.0 {
726 init_pos.y += 15.0;
727 msg = "DRG:".to_string()
728 + vec.to_pos2().x.to_string().as_str()
729 + ","
730 + vec.to_pos2().y.to_string().as_str();
731 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
732 }
733 }
734
735 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
736 #[cfg(feature = "puffin")]
737 puffin::profile_scope!("map_ui_paint_sub_components");
738 let zoom_slider = egui::Slider::new(
739 &mut self.zoom,
740 self.settings.min_zoom..=self.settings.max_zoom,
741 )
742 .show_value(false)
743 .orientation(SliderOrientation::Vertical);
744 let mut pos1 = rect.right_top();
745 let mut pos2 = rect.right_top();
746 pos1.x -= 80.0;
747 pos1.y += 120.0;
748 pos2.x -= 60.0;
749 pos2.y += 240.0;
750
751 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
752 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
753 ui_obj.scope_builder(ui_builder, |ui_obj| {
754 ui_obj.add(zoom_slider);
755 });
756 }
757
758 fn paint_map_points(
759 &self,
760 vec_points: &Vec<isize>,
761 hashm: &Option<HashMap<usize, MapPoint>>,
762 paint: &Painter,
763 ui_obj: &mut Ui,
764 min_point: &RawPoint,
765 resp: &Response,
766 ) -> Result<Vec<usize>, ()> {
767 let mut nearest_id = None;
768 let mut nodes_to_remove = Vec::new();
769 let mut shape_vec = vec![];
770
771 if hashm.is_none() {
772 return Err(());
773 }
774 if vec_points.is_empty() {
775 return Err(());
776 }
777 if self.settings.node_text_visibility == VisibilitySetting::Hover
779 && resp.hovered()
780 && let Some(point) = resp.hover_pos()
781 {
782 let raw_point = RawPoint::from(point);
783 let hovered_map_point = (*min_point + raw_point) / self.zoom;
784 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
785 &hovered_map_point.components,
786 1,
787 &squared_euclidean,
788 ) {
789 nearest_id = Some(nearest_node.first().unwrap().1);
790 }
791 }
792 let mut text_settings = TextSettings {
794 size: 12.00 * self.zoom,
795 anchor: Align2::LEFT_BOTTOM,
796 family: FontFamily::Proportional,
797 text: String::new(),
798 position: RawPoint::default(),
799 text_color: ui_obj.visuals().text_color(),
800 };
801
802 for temp_point in vec_points {
804 let parsed_point = temp_point.cast_unsigned();
805 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
806 #[cfg(feature = "puffin")]
807 puffin::profile_scope!("painting_points_m");
808 let viewport_point = system.raw_point * self.zoom - min_point;
809 if let Some(node_template) = &self.node_template {
810 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
811 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
812 }
813 } else if self.zoom > self.settings.label_visible_zoom
814 && self.settings.node_text_visibility == VisibilitySetting::Always
815 || (self.settings.node_text_visibility == VisibilitySetting::Hover
816 && nearest_id.unwrap_or(&0usize) == &system.get_id())
817 {
818 let mut viewport_text = viewport_point;
819 viewport_text.components[0] += 3.0 * self.zoom;
820 viewport_text.components[1] -= 3.0 * self.zoom;
821 text_settings.position = viewport_text;
822 text_settings.text = system.get_name();
823 self.paint_label(paint, &text_settings);
824 }
825
826 let system_id = system.get_id();
827 if let Some(init_time) = self.entities.get(&system_id) {
828 if let Some(template) = &self.node_template {
829 template.notification_ui(
830 ui_obj,
831 viewport_point.into(),
832 self.zoom,
833 *init_time,
834 self.current_style().alert_color,
835 );
836 } else if Animation::pulse(
837 paint,
838 viewport_point,
839 self.zoom,
840 *init_time,
841 self.current_style().alert_color,
842 ) {
843 ui_obj.ctx().request_repaint();
844 } else {
845 nodes_to_remove.push(system_id);
846 }
847 }
848 if let Some(node_template) = &self.node_template {
849 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
850 } else {
851 shape_vec.push(Shape::circle_filled(
852 viewport_point.into(),
853 4.00 * self.zoom,
854 self.current_style().fill_color,
855 ));
856 }
857 }
858 }
859 paint.extend(shape_vec);
860 Ok(nodes_to_remove)
861 }
862
863 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
864 #[cfg(feature = "puffin")]
865 puffin::profile_scope!("paint_map_lines");
866
867 if self.zoom > self.settings.line_visible_zoom
869 && let Some(mut stroke) = self.current_style().line
870 && let Some(segments) = &self.segments
871 {
872 let mut shape_vec = vec![];
873 let transparency_range = self.zoom - self.settings.line_visible_zoom;
874 if (0.00..0.80).contains(&transparency_range) {
875 let mut tup_stroke = stroke.color.to_tuple();
876 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
877 tup_stroke.3 = (255.0 * transparency).round() as u8;
878 let color = Color32::from_rgba_unmultiplied(
879 tup_stroke.0,
880 tup_stroke.1,
881 tup_stroke.2,
882 tup_stroke.3,
883 );
884 stroke = Stroke::new(stroke.width, color);
885 }
886 let center = self.current.pos / self.zoom;
890 let padding = stroke.width / self.zoom;
891 let half = RawPoint::new(
892 self.map_area.width() / 2.0 / self.zoom + padding,
893 self.map_area.height() / 2.0 / self.zoom + padding,
894 );
895 let query = rstar::AABB::from_corners(center - half, center + half);
896 for segment in segments.locate_in_envelope_intersecting(query) {
897 let pos_a = segment.raw_line.points[0] * self.zoom - min_point;
898 let pos_b = segment.raw_line.points[1] * self.zoom - min_point;
899 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
900 }
901 painter.extend(shape_vec);
902 }
903 }
904
905 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
906 #[cfg(feature = "puffin")]
907 puffin::profile_scope!("paint_label");
908 paint.text(
909 text_settings.position.into(),
910 text_settings.anchor,
911 text_settings.text.clone(),
912 FontId::new(text_settings.size, text_settings.family.clone()),
913 text_settings.text_color,
914 );
915 }
916
917 pub fn notify(&mut self, id_node: usize, time: Instant) {
924 #[cfg(feature = "puffin")]
925 puffin::profile_scope!("notify");
926 self.entities
927 .entry(id_node)
928 .and_modify(|value| *value = time)
929 .or_insert(time);
930 }
931
932 pub fn line_at(&self, point: [f32; 2], tolerance: f32) -> Option<Rc<str>> {
946 #[cfg(feature = "puffin")]
947 puffin::profile_scope!("line_at");
948 let segments = self.segments.as_ref()?;
949 let tolerance = tolerance.max(0.0);
950
951 let center = RawPoint::from(point);
952 let padding = RawPoint::new(tolerance, tolerance);
953 let query = rstar::AABB::from_corners(center - padding, center + padding);
954
955 let mut closest: Option<(f32, Rc<str>)> = None;
956 for segment in segments.locate_in_envelope_intersecting(query) {
957 let distance = segment.raw_line.distance_to_point(center);
958 if distance <= tolerance && closest.as_ref().is_none_or(|(best, _)| distance < *best) {
959 closest = Some((distance, Rc::clone(&segment.id)));
960 }
961 }
962 closest.map(|(_, id)| id)
963 }
964
965 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
968 self.menu_manager = Some(manager);
969 }
970
971 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
979 self.node_template = Some(template);
980 }
981
982 pub fn update_marker(&mut self, id: usize, node_id: usize) {
987 self.markers
988 .entry(id)
989 .and_modify(|value| *value = node_id)
990 .or_insert(node_id);
991 }
992
993 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
996 self.min_size = (width, height);
997 }
998
999 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
1002 self.max_size = (width, height);
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use std::time::Duration;
1010
1011 fn sample_points() -> Vec<MapPoint> {
1012 let mut map = Vec::new();
1013 map.push( MapPoint::new(1, RawPoint::new(0.0, 0.0)));
1014 map.push( MapPoint::new(2, RawPoint::new(10.0, 10.0)));
1015 map.push( MapPoint::new(3, RawPoint::new(-10.0, -10.0)));
1016 map
1017 }
1018
1019 #[test]
1022 fn map_new_initial_state() {
1023 let map = Map::new();
1024 assert_eq!(map.zoom, 1.0);
1025 assert_eq!(map.previous_zoom, 1.0);
1026 assert!(map.points.is_none());
1027 assert!(map.segments.is_none());
1028 assert!(map.tree.is_none());
1029 assert!(map.labels.is_empty());
1030 assert!(map.visible_points.is_empty());
1031 assert!(map.markers.is_empty());
1032 assert!(map.entities.is_empty());
1033 assert_eq!(map.min_size, (None, None));
1034 assert_eq!(map.max_size, (None, None));
1035 assert_eq!(map.current_index, 0);
1036 }
1037
1038 #[test]
1039 fn map_default_equals_new() {
1040 let map = Map::default();
1041 assert_eq!(map.zoom, 1.0);
1042 assert!(map.points.is_none());
1043 }
1044
1045 #[test]
1048 fn set_zoom_within_range() {
1049 let mut map = Map::new();
1050 map.set_zoom(1.5);
1051 assert_eq!(map.get_zoom(), 1.5);
1052 }
1053
1054 #[test]
1055 fn set_zoom_at_exact_limits() {
1056 let mut map = Map::new();
1057 map.set_zoom(map.settings.min_zoom);
1058 assert_eq!(map.get_zoom(), 0.1);
1059 map.set_zoom(map.settings.max_zoom);
1060 assert_eq!(map.get_zoom(), 2.0);
1061 }
1062
1063 #[test]
1064 fn set_zoom_out_of_range_is_ignored() {
1065 let mut map = Map::new();
1066 let initial = map.get_zoom();
1067 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
1069 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
1071 }
1072
1073 #[test]
1076 fn add_hashmap_points_computes_bounds() {
1077 let mut map = Map::new();
1078 map.add_points(sample_points());
1079
1080 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
1081 assert_eq!(map.reference.max.components, [10.0, 10.0]);
1082 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1084 assert_eq!(map.reference.dist, 3000.0);
1086 assert_eq!(map.current.min.components, map.reference.min.components);
1088 assert_eq!(map.current.max.components, map.reference.max.components);
1089 assert_eq!(map.current.pos.components, map.reference.pos.components);
1090 assert_eq!(map.current.dist, map.reference.dist);
1091 assert!(map.points.is_some());
1092 assert!(map.tree.is_some());
1093 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1094 }
1095
1096 #[test]
1097 fn add_hashmap_points_populates_visible_points() {
1098 let mut map = Map::new();
1099 map.add_points(sample_points());
1100 assert_eq!(map.visible_points.len(), 3);
1102 }
1103
1104 fn render_line_segments(map: &mut Map) -> Vec<[egui::Pos2; 2]> {
1107 use egui::{Context, RawInput, Shape};
1108 let ctx = Context::default();
1109 let input = RawInput {
1110 screen_rect: Some(egui::Rect::from_min_size(
1111 egui::Pos2::ZERO,
1112 egui::vec2(500.0, 500.0),
1113 )),
1114 ..RawInput::default()
1115 };
1116 let output = ctx.run_ui(input, |ui| {
1117 ui.add(&mut *map);
1118 });
1119 output
1120 .shapes
1121 .iter()
1122 .filter_map(|cs| match cs.shape {
1123 Shape::LineSegment { points, .. } => Some(points),
1124 _ => None,
1125 })
1126 .collect()
1127 }
1128
1129 #[test]
1130 fn segment_crossing_viewport_is_painted_even_with_far_endpoints() {
1131 let mut map = Map::new();
1136 map.set_zoom(1.0);
1137 let mut lines = Vec::new();
1138 lines.push(MapSegment::new(
1139 Rc::from("long"),
1140 RawPoint::new(-4000.0, -1.0),
1141 RawPoint::new(4000.0, 1.0),
1142 ));
1143 map.add_lines(lines);
1144 map.set_pos([0.0, 0.0]);
1145
1146 let segments = render_line_segments(&mut map);
1147 assert_eq!(segments.len(), 1);
1148 }
1149
1150 #[test]
1151 fn segment_outside_viewport_is_not_painted() {
1152 let mut map = Map::new();
1153 map.set_zoom(1.0);
1154 let mut lines = Vec::new();
1155 lines.push(MapSegment::new(
1156 Rc::from("far"),
1157 RawPoint::new(10_000.0, 10_000.0),
1158 RawPoint::new(10_100.0, 10_100.0),
1159 ));
1160 map.add_lines(lines);
1161 map.set_pos([0.0, 0.0]);
1162
1163 assert!(render_line_segments(&mut map).is_empty());
1164 }
1165
1166 #[test]
1167 fn add_lines_builds_segment_tree() {
1168 let mut map = Map::new();
1169 map.add_points(sample_points());
1170 let mut lines = Vec::new();
1171 lines.push(MapSegment::new(
1172 Rc::from("1-2"),
1173 RawPoint::new(0.0, 0.0),
1174 RawPoint::new(10.0, 10.0),
1175 ));
1176 map.add_lines(lines);
1177
1178 let tree = map
1179 .segments
1180 .as_ref()
1181 .expect("add_lines must build the segment tree");
1182 assert_eq!(tree.size(), 1);
1183
1184 let hit_query =
1187 rstar::AABB::from_corners(RawPoint::new(-1.0, -1.0), RawPoint::new(1.0, 1.0));
1188 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1189 assert_eq!(hits.len(), 1);
1190 assert_eq!(&*hits[0].id, "1-2");
1191
1192 let miss_query =
1193 rstar::AABB::from_corners(RawPoint::new(100.0, 100.0), RawPoint::new(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, RawPoint::new(0.0, 0.0));
1206 point_a.connections.push("a0".to_string());
1207 let mut point_b = MapPoint::new(1, RawPoint::new(50.0, 50.0));
1208 point_b.connections.push("a0".to_string());
1209
1210 let mut lines = Vec::new();
1211 lines.push(MapSegment::new(
1212 Rc::from("a0"),
1213 point_a.raw_point,
1214 point_b.raw_point,
1215 ));
1216
1217 let mut points = Vec::new();
1218 points.push(point_a);
1219 points.push(point_b);
1220 map.add_points(points);
1222 map.add_lines(lines);
1223
1224 map.set_pos([25.0, 25.0]);
1225
1226 let ctx = Context::default();
1228 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1229 let input = RawInput {
1230 screen_rect: Some(screen),
1231 ..RawInput::default()
1232 };
1233
1234 let output1 = ctx.run_ui(input.clone(), |ui| {
1235 ui.add(&mut map);
1236 });
1237
1238 let segments1: Vec<[egui::Pos2; 2]> = output1
1239 .shapes
1240 .iter()
1241 .filter_map(|cs| match cs.shape {
1242 Shape::LineSegment { points, .. } => Some(points),
1243 _ => None,
1244 })
1245 .collect();
1246
1247 assert!(
1248 !segments1.is_empty(),
1249 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
1250 );
1251
1252 let expected_a = egui::pos2(225.0, 225.0);
1255 let expected_b = egui::pos2(275.0, 275.0);
1256 let tolerance = 2.0;
1257 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
1258 let d_a1 = p1.distance(expected_a);
1259 let d_b1 = p2.distance(expected_b);
1260 let d_a2 = p2.distance(expected_a);
1261 let d_b2 = p1.distance(expected_b);
1262 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
1263 });
1264 assert!(
1265 found_on_frame1,
1266 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
1267 segments1
1268 );
1269
1270 let output2 = ctx.run_ui(input, |ui| {
1272 ui.add(&mut map);
1273 });
1274
1275 let segments2: Vec<[egui::Pos2; 2]> = output2
1276 .shapes
1277 .iter()
1278 .filter_map(|cs| match cs.shape {
1279 Shape::LineSegment { points, .. } => Some(points),
1280 _ => None,
1281 })
1282 .collect();
1283
1284 assert_eq!(
1285 segments1.len(),
1286 segments2.len(),
1287 "Frame 2: expected {} line segments (no duplication across frames), got {}",
1288 segments1.len(),
1289 segments2.len()
1290 );
1291 }
1292
1293 #[test]
1296 fn set_pos_and_get_pos_roundtrip() {
1297 let mut map = Map::new();
1298 map.set_pos([25.0, -35.0]);
1299 assert_eq!(map.get_pos(), [25.0, -35.0]);
1300 }
1301
1302 #[test]
1303 fn set_pos_from_nodeid_with_valid_id() {
1304 let mut map = Map::new();
1305 map.add_points(sample_points());
1306 map.set_pos_from_nodeid(2);
1307 assert_eq!(map.get_pos(), [10.0, 10.0]);
1308 }
1309
1310 #[test]
1311 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1312 let mut map = Map::new();
1313 map.add_points(sample_points());
1314 let before = map.reference.pos.components;
1315 map.set_pos_from_nodeid(999);
1316 assert_eq!(map.reference.pos.components, before);
1317 }
1318
1319 #[test]
1320 fn set_pos_from_nodeid_without_points_does_nothing() {
1321 let mut map = Map::new();
1322 map.set_pos_from_nodeid(1);
1323 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1324 }
1325
1326 #[test]
1329 fn add_labels_stores_labels() {
1330 let mut map = Map::new();
1331 let label = MapLabel {
1332 text: "Region".to_string(),
1333 center: Pos2::new(1.0, 2.0),
1334 };
1335 map.add_labels(vec![label]);
1336 assert_eq!(map.labels.len(), 1);
1337 assert_eq!(map.labels[0].text, "Region");
1338 }
1339
1340 #[test]
1341 fn add_lines_stores_lines() {
1342 let mut map = Map::new();
1343 let mut lines = Vec::new();
1344 lines.push(MapSegment::new(
1345 Rc::from("a-b"),
1346 RawPoint::new(0.0, 0.0),
1347 RawPoint::new(1.0, 1.0),
1348 ));
1349 map.add_lines(lines);
1350 let tree = map.segments.as_ref().unwrap();
1351 assert_eq!(tree.size(), 1);
1352 assert_eq!(
1353 &*tree
1354 .locate_in_envelope_intersecting(rstar::AABB::from_corners(
1355 RawPoint::new(-1.0, -1.0),
1356 RawPoint::new(2.0, 2.0),
1357 ))
1358 .next()
1359 .unwrap()
1360 .id,
1361 "a-b"
1362 );
1363 }
1364
1365 #[test]
1368 fn line_at_returns_closest_line_within_tolerance() {
1369 let mut map = Map::new();
1370 map.add_points(sample_points());
1371 let mut lines = Vec::new();
1372 lines.push(MapSegment::new(
1373 Rc::from("horizontal"),
1374 RawPoint::new(0.0, 0.0),
1375 RawPoint::new(10.0, 0.0),
1376 ));
1377 lines.push(MapSegment::new(
1378 Rc::from("vertical"),
1379 RawPoint::new(20.0, -5.0),
1380 RawPoint::new(20.0, 5.0),
1381 ));
1382 map.add_lines(lines);
1383
1384 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
1386 assert_eq!(&*hit, "horizontal");
1387
1388 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
1390 assert_eq!(&*hit, "vertical");
1391 }
1392
1393 #[test]
1394 fn line_at_returns_none_beyond_tolerance() {
1395 let mut map = Map::new();
1396 map.add_points(sample_points());
1397 let mut lines = Vec::new();
1398 lines.push(MapSegment::new(
1399 Rc::from("1-2"),
1400 RawPoint::new(0.0, 0.0),
1401 RawPoint::new(10.0, 10.0),
1402 ));
1403 map.add_lines(lines);
1404
1405 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
1408 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
1409 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
1410 }
1411
1412 #[test]
1413 fn line_at_returns_none_without_lines() {
1414 let map = Map::new();
1415 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
1416 }
1417
1418 #[test]
1419 fn line_at_negative_tolerance_behaves_like_zero() {
1420 let mut map = Map::new();
1421 map.add_points(sample_points());
1422 let mut lines = Vec::new();
1423 lines.push(MapSegment::new(
1424 Rc::from("1-2"),
1425 RawPoint::new(0.0, 0.0),
1426 RawPoint::new(10.0, 10.0),
1427 ));
1428 map.add_lines(lines);
1429
1430 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
1432 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
1433 }
1434
1435 #[test]
1436 fn notify_inserts_and_updates_entities() {
1437 let mut map = Map::new();
1438 let t1 = Instant::now();
1439 map.notify(5, t1);
1440 assert_eq!(map.entities.get(&5), Some(&t1));
1441
1442 let t2 = t1 + Duration::from_secs(1);
1443 map.notify(5, t2);
1444 assert_eq!(map.entities.get(&5), Some(&t2));
1445 assert_eq!(map.entities.len(), 1);
1446 }
1447
1448 #[test]
1449 fn update_marker_inserts_and_updates() {
1450 let mut map = Map::new();
1451 map.update_marker(1, 100);
1452 assert_eq!(map.markers.get(&1), Some(&100));
1453 map.update_marker(1, 200);
1454 assert_eq!(map.markers.get(&1), Some(&200));
1455 assert_eq!(map.markers.len(), 1);
1456 }
1457
1458 #[test]
1461 fn allocate_at_least_sets_min_size() {
1462 let mut map = Map::new();
1463 map.allocate_at_least(Some(100.0), None);
1464 assert_eq!(map.min_size, (Some(100.0), None));
1465 }
1466
1467 #[test]
1468 fn allocate_at_most_sets_max_size() {
1469 let mut map = Map::new();
1470 map.allocate_at_most(None, Some(200.0));
1471 assert_eq!(map.max_size, (None, Some(200.0)));
1472 }
1473
1474 #[test]
1477 fn adjust_bounds_scales_with_zoom() {
1478 let mut map = Map::new();
1479 map.reference.min = RawPoint::new(-10.0, -20.0);
1480 map.reference.max = RawPoint::new(10.0, 20.0);
1481 map.reference.pos = RawPoint::new(5.0, 5.0);
1482 map.reference.dist = 100.0;
1483 map.set_zoom(2.0);
1484 map.adjust_bounds();
1485
1486 assert_eq!(map.current.max.components, [20.0, 40.0]);
1487 assert_eq!(map.current.min.components, [-20.0, -40.0]);
1488 assert_eq!(map.current.pos.components, [10.0, 10.0]);
1489 assert_eq!(map.current.dist, 50.0);
1490 }
1491}