1use crate::map::animation::Animation;
75use crate::map::objects::{
76 ContextMenuManager, MapBounds, MapLabel, MapLine, MapPoint, MapSettings, RawLine, RawPoint,
77 TextSettings, VisibilitySetting,
78};
79use chrono;
80use egui::{epaint::CircleShape, widgets::*, *};
81use kdtree::KdTree;
82use kdtree::distance::squared_euclidean;
83use std::collections::{HashMap, HashSet};
84use std::fmt::Error;
85use std::rc::Rc;
86use std::time::Instant;
87
88use self::objects::NodeTemplate;
89
90pub mod animation;
91pub mod objects;
92
93#[derive(Clone)]
131pub struct Map {
132 zoom: f32,
133 previous_zoom: f32,
134 points: Option<HashMap<usize, MapPoint>>,
135 lines: Option<HashMap<String, MapLine>>,
136 labels: Vec<MapLabel>,
137 tree: Option<KdTree<f32, usize, [f32; 2]>>,
138 visible_points: Vec<isize>,
139 map_area: Rect,
140 reference: MapBounds,
141 current: MapBounds,
142 style: egui::Style,
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 visible_lines: HashSet<String>,
153 markers: HashMap<usize, usize>,
154}
155
156impl Default for Map {
157 fn default() -> Self {
159 Map::new()
160 }
161}
162
163impl Widget for &mut Map {
164 fn ui(self, ui: &mut egui::Ui) -> Response {
167 let rect = self.calculate_widget_dimentions(ui);
168
169 self.reference.dist = rect.distance();
171
172 self.assign_visual_style(ui);
173
174 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
175
176 let inner_response = canvas.show(ui, |ui| {
177 #[cfg(feature = "puffin")]
178 puffin::profile_scope!("paint_map");
179
180 if ui.is_rect_visible(self.map_area) {
181 let (resp, paint) =
182 ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
183 let vec = resp.drag_delta();
184 if vec.length() != 0.0 {
185 #[cfg(feature = "puffin")]
186 puffin::profile_scope!("calculating_points_in_visible_area");
187
188 let coords = RawPoint::from(vec.to_pos2());
189 let new_pos = self.reference.pos - (coords / self.zoom);
190 self.set_pos(new_pos.into());
191 }
192 let map_style = self.settings.styles[self.current_index].clone() * self.zoom;
193 if self.zoom < self.settings.line_visible_zoom {
194 let mut text_settings = TextSettings {
196 size: 12.00 * self.zoom * 2.00,
197 anchor: Align2::CENTER_CENTER,
198 family: FontFamily::Proportional,
199 text: String::new(),
200 position: RawPoint::default(),
201 text_color: ui.visuals().text_color(),
202 };
203 for label in &self.labels {
204 text_settings.text.clone_from(&label.text);
205 paint.text(
206 label.center,
207 Align2::CENTER_CENTER,
208 label.text.as_str(),
209 map_style.font.clone().unwrap(),
210 ui.visuals().text_color(),
211 );
212 self.paint_label(&paint, &text_settings);
213 }
214 }
215
216 let rect_midpoint = RawPoint::from(self.map_area.center());
220 let min_point = self.current.pos - rect_midpoint;
221 let vec_points = &self.visible_points;
222 let hashm = &self.points;
223 self.paint_map_lines(&paint, &min_point);
224
225 if let Ok(nodes_to_remove) =
226 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
227 {
228 for node in nodes_to_remove {
229 self.entities.remove(&node);
230 }
231 }
232
233 for marker in &self.markers {
234 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
235 let adjusted_point = point.raw_point * self.zoom - min_point;
236 if let Some(template) = &self.node_template {
237 template.marker_ui(ui, adjusted_point.into(), self.zoom);
238 } else {
239 let mut shapes = Vec::new();
240 let color = if ui.visuals().dark_mode {
241 Color32::LIGHT_GREEN
242 } else {
243 Color32::GREEN
244 };
245 let mut transparency =
246 (chrono::Local::now().timestamp_millis() % 2550) / 5;
247 if transparency > 255 {
248 transparency = 255 - (transparency - 255)
249 }
250 let corrected_color = Color32::from_rgba_unmultiplied(
251 color.r(),
252 color.g(),
253 color.b(),
254 transparency as u8,
255 );
256 shapes.push(Shape::Circle(CircleShape::stroke(
257 adjusted_point.into(),
258 4.0 * self.zoom,
259 Stroke::new(9.0 * self.zoom, corrected_color),
260 )));
261 ui.ctx().request_repaint();
262 ui.painter().extend(shapes);
263 }
264 }
265 }
266
267 self.paint_sub_components(ui, self.map_area);
268
269 self.capture_mouse_events(ui, &resp);
270
271 if self.zoom != self.previous_zoom {
272 #[cfg(feature = "puffin")]
273 puffin::profile_scope!("calculating viewport with zoom");
274 self.adjust_bounds();
275 self.calculate_visible_points();
276 self.previous_zoom = self.zoom;
277 }
278
279 if let Some(menu_mon) = &mut self.menu_manager {
280 resp.context_menu(|ui| {
281 menu_mon.ui(ui);
282 });
283 }
284
285 if cfg!(debug_assertions) {
286 self.print_debug_info(paint, resp);
287 }
288 }
289 });
290 ui.allocate_space(self.map_area.size());
291 inner_response.response
292 }
293}
294
295impl Map {
296 pub fn new() -> Self {
301 let settings = MapSettings::default();
302 Self {
303 zoom: 1.0,
304 previous_zoom: 1.0,
305 map_area: Rect::NOTHING,
306 tree: None,
307 points: None,
308 lines: None,
309 labels: Vec::new(),
310 visible_points: Vec::new(),
311 current: MapBounds::default(),
312 reference: MapBounds::default(),
313 settings,
314 min_size: (None, None),
315 max_size: (None, None),
316 current_index: 0,
317 entities: HashMap::new(),
318 style: egui::Style::default(),
319 menu_manager: None,
320 node_template: None,
321 visible_lines: HashSet::new(),
322 markers: HashMap::new(),
323 }
324 }
325
326 fn calculate_widget_dimentions(&mut self, ui: &mut Ui) -> RawLine {
327 self.map_area = ui.available_rect_before_wrap();
328 let mut left_top = RawPoint::from(self.map_area.left_top());
329 let mut right_bottom = RawPoint::from(self.map_area.right_bottom());
330 if let Some(val) = self.max_size.0
331 && right_bottom.components[0] > self.max_size.0.unwrap_or(0.0f32)
332 {
333 right_bottom.components[0] = val;
334 }
335 if let Some(val) = self.max_size.1
336 && right_bottom.components[1] > self.max_size.1.unwrap_or(0.0f32)
337 {
338 right_bottom.components[1] = val;
339 }
340 if let Some(val) = self.min_size.0
341 && left_top.components[0] < self.min_size.0.unwrap_or(0.0f32)
342 {
343 left_top.components[0] = val;
344 }
345 if let Some(val) = self.min_size.1
346 && left_top.components[1] < self.min_size.1.unwrap_or(0.0f32)
347 {
348 left_top.components[1] = val;
349 }
350 RawLine::new(left_top, right_bottom)
351 }
352
353 fn calculate_visible_points(&mut self) {
354 #[cfg(feature = "puffin")]
355 puffin::profile_scope!("calculate_visible_points");
356 if self.current.dist > 0.0
357 && self.current.dist < f32::INFINITY
358 && let Some(tree) = &self.tree
359 {
360 let center = self.current.pos / self.zoom;
361 let radius = self.current.dist.powi(2);
362 let point: [f32; 2] = center.into();
363 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
364 self.visible_points.clear();
365 for point in vis_pos {
366 self.visible_points.push(point.1.cast_signed());
367 let system = self.points.as_ref().unwrap().get(point.1);
368 for connection in &system.unwrap().connections {
369 if !self.visible_lines.contains(&connection.clone()) {
370 self.visible_lines.insert(connection.clone());
371 }
372 }
373 }
374 }
375 }
376
377 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
406 #[cfg(feature = "puffin")]
407 puffin::profile_scope!("add_hashmap_points");
408 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
409 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
410 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
411 let mut h_map = hash_map.clone();
412
413 for entry in h_map.iter_mut() {
414 for i in 0..min.components.len() {
415 if entry.1.raw_point.components[i] < min.components[i] {
416 min.components[i] = entry.1.raw_point.components[i];
417 }
418 if entry.1.raw_point.components[i] > max.components[i] {
419 max.components[i] = entry.1.raw_point.components[i];
420 }
421 }
422 let _result = tree.add(entry.1.raw_point.into(), *entry.0);
423 }
424
425 self.reference.min = min;
427 self.reference.max = max;
428 self.points = Some(h_map);
429 self.tree = Some(tree);
430 self.reference.pos = RawLine::new(min, max).midpoint();
431 if self.map_area.area() == 0.0 {
435 self.reference.dist = 3000.00;
436 } else {
437 let rect = RawLine::new(
438 RawPoint::from(self.map_area.left_top()),
439 RawPoint::from(self.map_area.right_bottom()),
440 );
441 self.reference.dist = rect.distance();
442 }
443 self.current = self.reference.clone();
444 self.calculate_visible_points();
445 }
446
447 pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
452 #[cfg(feature = "puffin")]
453 puffin::profile_scope!("set_pos_from_nodeid");
454 if let Some(hash_map) = &self.points
455 && let Some(map_point) = hash_map.get(&node_id)
456 {
457 self.reference.pos = map_point.raw_point;
458 self.adjust_bounds();
459 self.calculate_visible_points();
460 }
461 }
462
463 pub fn set_pos(&mut self, position: [f32; 2]) {
465 #[cfg(feature = "puffin")]
466 puffin::profile_scope!("set_pos");
467 let point = RawPoint::from(position);
468 self.reference.pos = point;
469 self.adjust_bounds();
470 self.calculate_visible_points();
471 }
472
473 pub fn get_pos(self) -> [f32; 2] {
478 #[cfg(feature = "puffin")]
479 puffin::profile_scope!("get_pos");
480 self.reference.pos.into()
481 }
482
483 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
488 #[cfg(feature = "puffin")]
489 puffin::profile_scope!("add_labels");
490 self.labels = labels;
491 }
492
493 pub fn add_lines(&mut self, lines: HashMap<String, MapLine>) {
504 #[cfg(feature = "puffin")]
505 puffin::profile_scope!("add_lines");
506 self.lines = Some(lines);
507 }
508
509 fn adjust_bounds(&mut self) {
510 #[cfg(feature = "puffin")]
511 puffin::profile_scope!("adjust_bounds");
512 self.current.max = self.reference.max * self.zoom;
513 self.current.min = self.reference.min * self.zoom;
514 self.current.dist = self.reference.dist / self.zoom;
515 self.current.pos = self.reference.pos * self.zoom;
516 }
517
518 fn capture_mouse_events(&mut self, ui: &Ui, resp: &Response) {
519 #[cfg(feature = "puffin")]
520 puffin::profile_scope!("capture_mouse_events");
521 if ui.rect_contains_pointer(self.map_area) {
523 ui.input(|x| {
524 #[cfg(feature = "puffin")]
525 puffin::profile_scope!("capture_mouse_events");
526
527 if !x.events.is_empty() {
528 for event in &x.events {
529 match event {
530 Event::MouseWheel {
531 unit: _,
532 delta,
533 modifiers,
534 phase: _,
535 } => {
536 #[cfg(target_os = "macos")]
537 let zoom_modifier = if modifiers.mac_cmd {
538 delta.y / 80.00
539 } else {
540 delta.y / 400.00
541 };
542
543 #[cfg(not(target_os = "macos"))]
544 let zoom_modifier = if modifiers.ctrl {
545 delta.y / 8.00
546 } else {
547 delta.y / 40.00
548 };
549
550 let mut pre_zoom = self.zoom + zoom_modifier;
551 if pre_zoom > self.settings.max_zoom {
552 pre_zoom = self.settings.max_zoom;
553 }
554 if pre_zoom < self.settings.min_zoom {
555 pre_zoom = self.settings.min_zoom;
556 }
557 self.zoom = pre_zoom;
558 }
559 _ => {
560 continue;
561 }
562 };
563 }
564 }
565 });
566 }
567 if resp.secondary_clicked() {}
568 }
569
570 pub fn set_zoom(&mut self, value: f32) {
575 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
576 self.zoom = value;
577 }
578 }
579
580 pub fn get_zoom(&mut self) -> f32 {
582 self.zoom
583 }
584
585 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
586 let style_index = ui_obj.visuals().dark_mode as usize;
587
588 if self.current_index != style_index {
589 #[cfg(feature = "puffin")]
590 puffin::profile_scope!("asign_visual_style");
591
592 self.current_index = style_index;
593 self.style = ui_obj.style_mut().clone();
594 self.style.visuals.extreme_bg_color =
595 self.settings.styles[style_index].background_color;
596 self.style.visuals.window_stroke = self.settings.styles[style_index].border.unwrap();
597 }
598 }
599
600 fn print_debug_info(&mut self, paint: Painter, resp: Response) {
601 #[cfg(feature = "puffin")]
602 puffin::profile_scope!("printing debug data");
603
604 let mut init_pos = Pos2::new(
605 self.map_area.left_top().x + 10.00,
606 self.map_area.left_top().y + 10.00,
607 );
608 let mut msg = "MIN:".to_string()
609 + self.current.min.components[0].to_string().as_str()
610 + ","
611 + self.current.min.components[1].to_string().as_str();
612 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
613 init_pos.y += 15.0;
614 msg = "MAX:".to_string()
615 + self.current.max.components[0].to_string().as_str()
616 + ","
617 + self.current.max.components[1].to_string().as_str();
618 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
619 init_pos.y += 15.0;
620 msg = "CUR:(".to_string()
621 + self.current.pos.components[0].to_string().as_str()
622 + ","
623 + self.current.pos.components[1].to_string().as_str()
624 + ")";
625 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
626 init_pos.y += 15.0;
627 msg = "DST:".to_string() + self.current.dist.to_string().as_str();
628 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
629 init_pos.y += 15.0;
630 msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
631 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
632 init_pos.y += 15.0;
633 msg = "REC:(".to_string()
634 + self.map_area.left_top().x.to_string().as_str()
635 + ","
636 + self.map_area.left_top().y.to_string().as_str()
637 + "),("
638 + self.map_area.right_bottom().x.to_string().as_str()
639 + ","
640 + self.map_area.right_bottom().y.to_string().as_str()
641 + ")";
642 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
643 if let Some(points) = &self.points {
644 init_pos.y += 15.0;
645 msg = "NUM:".to_string() + points.len().to_string().as_str();
646 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
647 }
648 if !self.visible_points.is_empty() {
649 init_pos.y += 15.0;
650 msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
651 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
652 }
653 if let Some(pointer_pos) = resp.hover_pos() {
654 init_pos.y += 15.0;
655 msg = "HVR:".to_string()
656 + pointer_pos.x.to_string().as_str()
657 + ","
658 + pointer_pos.y.to_string().as_str();
659 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
660 }
661 let vec = resp.drag_delta();
662 if vec.length() != 0.0 {
663 init_pos.y += 15.0;
664 msg = "DRG:".to_string()
665 + vec.to_pos2().x.to_string().as_str()
666 + ","
667 + vec.to_pos2().y.to_string().as_str();
668 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
669 }
670 }
671
672 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
673 #[cfg(feature = "puffin")]
674 puffin::profile_scope!("map_ui_paint_sub_components");
675 let zoom_slider = egui::Slider::new(
676 &mut self.zoom,
677 self.settings.min_zoom..=self.settings.max_zoom,
678 )
679 .show_value(false)
680 .orientation(SliderOrientation::Vertical);
682 let mut pos1 = rect.right_top();
683 let mut pos2 = rect.right_top();
684 pos1.x -= 80.0;
685 pos1.y += 120.0;
686 pos2.x -= 60.0;
687 pos2.y += 240.0;
688
689 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
691 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
693 ui_obj.scope_builder(ui_builder, |ui_obj| {
695 ui_obj.add(zoom_slider);
696 });
697 }
698
699 fn paint_map_points(
700 &self,
701 vec_points: &Vec<isize>,
702 hashm: &Option<HashMap<usize, MapPoint>>,
703 paint: &Painter,
704 ui_obj: &mut Ui,
705 min_point: &RawPoint,
706 resp: &Response,
707 ) -> Result<Vec<usize>, ()> {
708 let mut nearest_id = None;
709 let mut nodes_to_remove = Vec::new();
710 let mut shape_vec = vec![];
711
712 if hashm.is_none() {
713 return Err(());
714 }
715 if vec_points.is_empty() {
716 return Err(());
717 }
718 if self.settings.node_text_visibility == VisibilitySetting::Hover
720 && resp.hovered()
721 && let Some(point) = resp.hover_pos()
722 {
723 let raw_point = RawPoint::from(point);
724 let hovered_map_point = (*min_point + raw_point) / self.zoom;
725 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
726 &hovered_map_point.components,
727 1,
728 &squared_euclidean,
729 ) {
730 nearest_id = Some(nearest_node.first().unwrap().1);
731 }
732 }
733 let mut text_settings = TextSettings {
735 size: 12.00 * self.zoom,
736 anchor: Align2::LEFT_BOTTOM,
737 family: FontFamily::Proportional,
738 text: String::new(),
739 position: RawPoint::default(),
740 text_color: ui_obj.visuals().text_color(),
741 };
742
743 for temp_point in vec_points {
745 let parsed_point = temp_point.cast_unsigned();
746 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
747 #[cfg(feature = "puffin")]
748 puffin::profile_scope!("painting_points_m");
749 let viewport_point = system.raw_point * self.zoom - min_point;
750 if let Some(node_template) = &self.node_template {
751 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
752 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
753 }
754 } else if self.zoom > self.settings.label_visible_zoom
755 && self.settings.node_text_visibility == VisibilitySetting::Allways
756 || (self.settings.node_text_visibility == VisibilitySetting::Hover
757 && nearest_id.unwrap_or(&0usize) == &system.get_id())
758 {
759 let mut viewport_text = viewport_point;
760 viewport_text.components[0] += 3.0 * self.zoom;
761 viewport_text.components[1] -= 3.0 * self.zoom;
762 text_settings.position = viewport_text;
763 text_settings.text = system.get_name();
764 self.paint_label(paint, &text_settings);
765 }
766
767 let system_id = system.get_id();
768 if let Some(init_time) = self.entities.get(&system_id) {
769 if let Some(template) = &self.node_template {
770 template.notification_ui(
771 ui_obj,
772 viewport_point.into(),
773 self.zoom,
774 *init_time,
775 self.settings.styles[self.current_index].alert_color,
776 );
777 } else {
778 match Animation::pulse(
779 paint,
780 viewport_point,
781 self.zoom,
782 *init_time,
783 self.settings.styles[self.current_index].alert_color,
784 ) {
785 Ok(true) => {
786 ui_obj.ctx().request_repaint();
787 }
788 Ok(false) => nodes_to_remove.push(system_id),
789 Err(_) => (),
790 }
791 }
792 }
793 if let Some(node_template) = &self.node_template {
794 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
795 } else {
796 shape_vec.push(Shape::circle_filled(
797 viewport_point.into(),
798 4.00 * self.zoom,
799 self.settings.styles[self.current_index].fill_color,
800 ));
801 }
802 }
803 }
804 paint.extend(shape_vec);
805 Ok(nodes_to_remove)
806 }
807
808 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
809 #[cfg(feature = "puffin")]
810 puffin::profile_scope!("paint_map_lines");
811
812 if self.zoom > self.settings.line_visible_zoom {
814 let mut shape_vec = vec![];
815 let mut stroke = self.settings.styles[self.current_index].line.unwrap();
816 let transparency_range = self.zoom - self.settings.line_visible_zoom;
817 if (0.00..0.80).contains(&transparency_range) {
818 let mut tup_stroke = self.settings.styles[self.current_index]
819 .line
820 .unwrap()
821 .color
822 .to_tuple();
823 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
824 tup_stroke.3 = (255.0 * transparency).round() as u8;
825 let color = Color32::from_rgba_unmultiplied(
826 tup_stroke.0,
827 tup_stroke.1,
828 tup_stroke.2,
829 tup_stroke.3,
830 );
831 stroke = Stroke::new(
832 self.settings.styles[self.current_index].line.unwrap().width,
833 color,
834 );
835 }
836 for line in &self.visible_lines {
838 if let Some(connection) = self.lines.as_ref().unwrap().get(line) {
839 let pos_a = connection.raw_line.points[0] * self.zoom - min_point;
840 let pos_b = connection.raw_line.points[1] * self.zoom - min_point;
841 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
844 }
846 }
847 painter.extend(shape_vec);
848 }
849 }
850
851 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
852 #[cfg(feature = "puffin")]
853 puffin::profile_scope!("paint_label");
854 paint.text(
855 text_settings.position.into(),
856 text_settings.anchor,
857 text_settings.text.clone(),
858 FontId::new(text_settings.size, text_settings.family.clone()),
859 text_settings.text_color,
860 );
861 }
862
863 pub fn notify(&mut self, id_node: usize, time: Instant) -> Result<bool, Error> {
872 #[cfg(feature = "puffin")]
873 puffin::profile_scope!("notify");
874 self.entities
875 .entry(id_node)
876 .and_modify(|value| *value = time)
877 .or_insert(time);
878 Ok(true)
879 }
880
881 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
884 self.menu_manager = Some(manager);
885 }
886
887 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
895 self.node_template = Some(template);
896 }
897
898 pub fn update_marker(&mut self, id: usize, node_id: usize) {
903 self.markers
904 .entry(id)
905 .and_modify(|value| *value = node_id)
906 .or_insert(node_id);
907 }
908
909 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
912 self.min_size = (width, height);
913 }
914
915 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
918 self.max_size = (width, height);
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use std::time::Duration;
926
927 fn sample_points() -> HashMap<usize, MapPoint> {
928 let mut map = HashMap::new();
929 map.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
930 map.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
931 map.insert(3, MapPoint::new(3, RawPoint::new(-10.0, -10.0)));
932 map
933 }
934
935 #[test]
938 fn map_new_initial_state() {
939 let map = Map::new();
940 assert_eq!(map.zoom, 1.0);
941 assert_eq!(map.previous_zoom, 1.0);
942 assert!(map.points.is_none());
943 assert!(map.lines.is_none());
944 assert!(map.tree.is_none());
945 assert!(map.labels.is_empty());
946 assert!(map.visible_points.is_empty());
947 assert!(map.visible_lines.is_empty());
948 assert!(map.markers.is_empty());
949 assert!(map.entities.is_empty());
950 assert_eq!(map.min_size, (None, None));
951 assert_eq!(map.max_size, (None, None));
952 assert_eq!(map.current_index, 0);
953 }
954
955 #[test]
956 fn map_default_equals_new() {
957 let map = Map::default();
958 assert_eq!(map.zoom, 1.0);
959 assert!(map.points.is_none());
960 }
961
962 #[test]
965 fn set_zoom_within_range() {
966 let mut map = Map::new();
967 map.set_zoom(1.5);
968 assert_eq!(map.get_zoom(), 1.5);
969 }
970
971 #[test]
972 fn set_zoom_at_exact_limits() {
973 let mut map = Map::new();
974 map.set_zoom(map.settings.min_zoom);
975 assert_eq!(map.get_zoom(), 0.1);
976 map.set_zoom(map.settings.max_zoom);
977 assert_eq!(map.get_zoom(), 2.0);
978 }
979
980 #[test]
981 fn set_zoom_out_of_range_is_ignored() {
982 let mut map = Map::new();
983 let initial = map.get_zoom();
984 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
986 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
988 }
989
990 #[test]
993 fn add_hashmap_points_computes_bounds() {
994 let mut map = Map::new();
995 map.add_hashmap_points(sample_points());
996
997 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
998 assert_eq!(map.reference.max.components, [10.0, 10.0]);
999 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1001 assert_eq!(map.reference.dist, 3000.0);
1003 assert_eq!(map.current.min.components, map.reference.min.components);
1005 assert_eq!(map.current.max.components, map.reference.max.components);
1006 assert_eq!(map.current.pos.components, map.reference.pos.components);
1007 assert_eq!(map.current.dist, map.reference.dist);
1008 assert!(map.points.is_some());
1009 assert!(map.tree.is_some());
1010 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1011 }
1012
1013 #[test]
1014 fn add_hashmap_points_populates_visible_points() {
1015 let mut map = Map::new();
1016 map.add_hashmap_points(sample_points());
1017 assert_eq!(map.visible_points.len(), 3);
1019 }
1020
1021 #[test]
1022 fn add_hashmap_points_populates_visible_lines() {
1023 let mut points = sample_points();
1024 points
1025 .get_mut(&1)
1026 .unwrap()
1027 .connections
1028 .push("1-2".to_string());
1029 let mut map = Map::new();
1030 map.add_hashmap_points(points);
1031 assert!(map.visible_lines.contains("1-2"));
1032 }
1033
1034 #[test]
1037 fn set_pos_and_get_pos_roundtrip() {
1038 let mut map = Map::new();
1039 map.set_pos([25.0, -35.0]);
1040 assert_eq!(map.get_pos(), [25.0, -35.0]);
1041 }
1042
1043 #[test]
1044 fn set_pos_from_nodeid_with_valid_id() {
1045 let mut map = Map::new();
1046 map.add_hashmap_points(sample_points());
1047 map.set_pos_from_nodeid(2);
1048 assert_eq!(map.get_pos(), [10.0, 10.0]);
1049 }
1050
1051 #[test]
1052 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1053 let mut map = Map::new();
1054 map.add_hashmap_points(sample_points());
1055 let before = map.reference.pos.components;
1056 map.set_pos_from_nodeid(999);
1057 assert_eq!(map.reference.pos.components, before);
1058 }
1059
1060 #[test]
1061 fn set_pos_from_nodeid_without_points_does_nothing() {
1062 let mut map = Map::new();
1063 map.set_pos_from_nodeid(1);
1064 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1065 }
1066
1067 #[test]
1070 fn add_labels_stores_labels() {
1071 let mut map = Map::new();
1072 let label = MapLabel {
1073 text: "Region".to_string(),
1074 center: Pos2::new(1.0, 2.0),
1075 };
1076 map.add_labels(vec![label]);
1077 assert_eq!(map.labels.len(), 1);
1078 assert_eq!(map.labels[0].text, "Region");
1079 }
1080
1081 #[test]
1082 fn add_lines_stores_lines() {
1083 let mut map = Map::new();
1084 let mut lines = HashMap::new();
1085 lines.insert(
1086 "a-b".to_string(),
1087 MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(1.0, 1.0)),
1088 );
1089 map.add_lines(lines);
1090 assert!(map.lines.as_ref().unwrap().contains_key("a-b"));
1091 }
1092
1093 #[test]
1096 fn notify_inserts_and_updates_entities() {
1097 let mut map = Map::new();
1098 let t1 = Instant::now();
1099 assert!(map.notify(5, t1).is_ok());
1100 assert_eq!(map.entities.get(&5), Some(&t1));
1101
1102 let t2 = t1 + Duration::from_secs(1);
1103 assert!(map.notify(5, t2).is_ok());
1104 assert_eq!(map.entities.get(&5), Some(&t2));
1105 assert_eq!(map.entities.len(), 1);
1106 }
1107
1108 #[test]
1109 fn update_marker_inserts_and_updates() {
1110 let mut map = Map::new();
1111 map.update_marker(1, 100);
1112 assert_eq!(map.markers.get(&1), Some(&100));
1113 map.update_marker(1, 200);
1114 assert_eq!(map.markers.get(&1), Some(&200));
1115 assert_eq!(map.markers.len(), 1);
1116 }
1117
1118 #[test]
1121 fn allocate_at_least_sets_min_size() {
1122 let mut map = Map::new();
1123 map.allocate_at_least(Some(100.0), None);
1124 assert_eq!(map.min_size, (Some(100.0), None));
1125 }
1126
1127 #[test]
1128 fn allocate_at_most_sets_max_size() {
1129 let mut map = Map::new();
1130 map.allocate_at_most(None, Some(200.0));
1131 assert_eq!(map.max_size, (None, Some(200.0)));
1132 }
1133
1134 #[test]
1137 fn adjust_bounds_scales_with_zoom() {
1138 let mut map = Map::new();
1139 map.reference.min = RawPoint::new(-10.0, -20.0);
1140 map.reference.max = RawPoint::new(10.0, 20.0);
1141 map.reference.pos = RawPoint::new(5.0, 5.0);
1142 map.reference.dist = 100.0;
1143 map.set_zoom(2.0);
1144 map.adjust_bounds();
1145
1146 assert_eq!(map.current.max.components, [20.0, 40.0]);
1147 assert_eq!(map.current.min.components, [-20.0, -40.0]);
1148 assert_eq!(map.current.pos.components, [10.0, 10.0]);
1149 assert_eq!(map.current.dist, 50.0);
1150 }
1151}