1use crate::map::animation::Animation;
2use crate::map::objects::{
3 ContextMenuManager, MapBounds, MapLabel, MapLine, MapPoint, MapSettings, RawLine, RawPoint,
4 TextSettings, VisibilitySetting,
5};
6use chrono;
7use egui::{epaint::CircleShape, widgets::*, *};
8use kdtree::KdTree;
9use kdtree::distance::squared_euclidean;
10use std::collections::{HashMap, HashSet};
11use std::fmt::Error;
12use std::rc::Rc;
13use std::time::Instant;
14
15use self::objects::NodeTemplate;
16
17pub mod animation;
18pub mod objects;
19#[derive(Clone)]
23pub struct Map {
24 zoom: f32,
25 previous_zoom: f32,
26 points: Option<HashMap<usize, MapPoint>>,
27 lines: Option<HashMap<String, MapLine>>,
28 labels: Vec<MapLabel>,
29 tree: Option<KdTree<f32, usize, [f32; 2]>>,
30 visible_points: Vec<isize>,
31 map_area: Rect,
32 reference: MapBounds,
33 current: MapBounds,
34 style: egui::Style,
35 current_index: usize,
36 entities: HashMap<usize, Instant>,
37 min_size: (Option<f32>, Option<f32>),
38 max_size: (Option<f32>, Option<f32>),
39 pub settings: MapSettings,
40 menu_manager: Option<Rc<dyn ContextMenuManager>>,
41 node_template: Option<Rc<dyn NodeTemplate>>,
42 visible_lines: HashSet<String>,
43 markers: HashMap<usize, usize>,
44}
45
46impl Default for Map {
47 fn default() -> Self {
48 Map::new()
49 }
50}
51
52impl Widget for &mut Map {
53 fn ui(self, ui: &mut egui::Ui) -> Response {
54 let rect = self.calculate_widget_dimentions(ui);
55
56 self.reference.dist = rect.distance();
58
59 self.assign_visual_style(ui);
60
61 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
62
63 let inner_response = canvas.show(ui, |ui| {
64 #[cfg(feature = "puffin")]
65 puffin::profile_scope!("paint_map");
66
67 if ui.is_rect_visible(self.map_area) {
68 let (resp, paint) =
69 ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
70 let vec = resp.drag_delta();
71 if vec.length() != 0.0 {
72 #[cfg(feature = "puffin")]
73 puffin::profile_scope!("calculating_points_in_visible_area");
74
75 let coords = RawPoint::from(vec.to_pos2());
76 let new_pos = self.reference.pos - (coords / self.zoom);
77 self.set_pos(new_pos.into());
78 }
79 let map_style = self.settings.styles[self.current_index].clone() * self.zoom;
80 if self.zoom < self.settings.line_visible_zoom {
81 let mut text_settings = TextSettings {
83 size: 12.00 * self.zoom * 2.00,
84 anchor: Align2::CENTER_CENTER,
85 family: FontFamily::Proportional,
86 text: String::new(),
87 position: RawPoint::default(),
88 text_color: ui.visuals().text_color(),
89 };
90 for label in &self.labels {
91 text_settings.text.clone_from(&label.text);
92 paint.text(
93 label.center,
94 Align2::CENTER_CENTER,
95 label.text.as_str(),
96 map_style.font.clone().unwrap(),
97 ui.visuals().text_color(),
98 );
99 self.paint_label(&paint, &text_settings);
100 }
101 }
102
103 let rect_midpoint = RawPoint::from(self.map_area.center());
107 let min_point = self.current.pos - rect_midpoint;
108 let vec_points = &self.visible_points;
109 let hashm = &self.points;
110 self.paint_map_lines(&paint, &min_point);
111
112 if let Ok(nodes_to_remove) =
113 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
114 {
115 for node in nodes_to_remove {
116 self.entities.remove(&node);
117 }
118 }
119
120 for marker in &self.markers {
121 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
122 let adjusted_point = point.raw_point * self.zoom - min_point;
123 if let Some(template) = &self.node_template {
124 template.marker_ui(ui, adjusted_point.into(), self.zoom);
125 } else {
126 let mut shapes = Vec::new();
127 let color = if ui.visuals().dark_mode {
128 Color32::LIGHT_GREEN
129 } else {
130 Color32::GREEN
131 };
132 let mut transparency =
133 (chrono::Local::now().timestamp_millis() % 2550) / 5;
134 if transparency > 255 {
135 transparency = 255 - (transparency - 255)
136 }
137 let corrected_color = Color32::from_rgba_unmultiplied(
138 color.r(),
139 color.g(),
140 color.b(),
141 transparency as u8,
142 );
143 shapes.push(Shape::Circle(CircleShape::stroke(
144 adjusted_point.into(),
145 4.0 * self.zoom,
146 Stroke::new(9.0 * self.zoom, corrected_color),
147 )));
148 ui.ctx().request_repaint();
149 ui.painter().extend(shapes);
150 }
151 }
152 }
153
154 self.paint_sub_components(ui, self.map_area);
155
156 self.capture_mouse_events(ui, &resp);
157
158 if self.zoom != self.previous_zoom {
159 #[cfg(feature = "puffin")]
160 puffin::profile_scope!("calculating viewport with zoom");
161 self.adjust_bounds();
162 self.calculate_visible_points();
163 self.previous_zoom = self.zoom;
164 }
165
166 if let Some(menu_mon) = &mut self.menu_manager {
167 resp.context_menu(|ui| {
168 menu_mon.ui(ui);
169 });
170 }
171
172 if cfg!(debug_assertions) {
173 self.print_debug_info(paint, resp);
174 }
175 }
176 });
177 ui.allocate_space(self.map_area.size());
178 inner_response.response
179 }
180}
181
182impl Map {
183 pub fn new() -> Self {
184 let settings = MapSettings::default();
185 Self {
186 zoom: 1.0,
187 previous_zoom: 1.0,
188 map_area: Rect::NOTHING,
189 tree: None,
190 points: None,
191 lines: None,
192 labels: Vec::new(),
193 visible_points: Vec::new(),
194 current: MapBounds::default(),
195 reference: MapBounds::default(),
196 settings,
197 min_size: (None, None),
198 max_size: (None, None),
199 current_index: 0,
200 entities: HashMap::new(),
201 style: egui::Style::default(),
202 menu_manager: None,
203 node_template: None,
204 visible_lines: HashSet::new(),
205 markers: HashMap::new(),
206 }
207 }
208
209 fn calculate_widget_dimentions(&mut self, ui: &mut Ui) -> RawLine {
210 self.map_area = ui.available_rect_before_wrap();
211 let mut left_top = RawPoint::from(self.map_area.left_top());
212 let mut right_bottom = RawPoint::from(self.map_area.right_bottom());
213 if let Some(val) = self.max_size.0
214 && right_bottom.components[0] > self.max_size.0.unwrap_or(0.0f32)
215 {
216 right_bottom.components[0] = val;
217 }
218 if let Some(val) = self.max_size.1
219 && right_bottom.components[1] > self.max_size.1.unwrap_or(0.0f32)
220 {
221 right_bottom.components[1] = val;
222 }
223 if let Some(val) = self.min_size.0
224 && left_top.components[0] < self.min_size.0.unwrap_or(0.0f32)
225 {
226 left_top.components[0] = val;
227 }
228 if let Some(val) = self.min_size.1
229 && left_top.components[1] < self.min_size.1.unwrap_or(0.0f32)
230 {
231 left_top.components[1] = val;
232 }
233 RawLine::new(left_top, right_bottom)
234 }
235
236 fn calculate_visible_points(&mut self) {
237 #[cfg(feature = "puffin")]
238 puffin::profile_scope!("calculate_visible_points");
239 if self.current.dist > 0.0
240 && self.current.dist < f32::INFINITY
241 && let Some(tree) = &self.tree
242 {
243 let center = self.current.pos / self.zoom;
244 let radius = self.current.dist.powi(2);
245 let point: [f32; 2] = center.into();
246 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
247 self.visible_points.clear();
248 for point in vis_pos {
249 self.visible_points.push(point.1.cast_signed());
250 let system = self.points.as_ref().unwrap().get(point.1);
251 for connection in &system.unwrap().connections {
252 if !self.visible_lines.contains(&connection.clone()) {
253 self.visible_lines.insert(connection.clone());
254 }
255 }
256 }
257 }
258 }
259
260 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
261 #[cfg(feature = "puffin")]
262 puffin::profile_scope!("add_hashmap_points");
263 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
264 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
265 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
266 let mut h_map = hash_map.clone();
267
268 for entry in h_map.iter_mut() {
269 for i in 0..min.components.len() {
270 if entry.1.raw_point.components[i] < min.components[i] {
271 min.components[i] = entry.1.raw_point.components[i];
272 }
273 if entry.1.raw_point.components[i] > max.components[i] {
274 max.components[i] = entry.1.raw_point.components[i];
275 }
276 }
277 let _result = tree.add(entry.1.raw_point.into(), *entry.0);
278 }
279
280 self.reference.min = min;
282 self.reference.max = max;
283 self.points = Some(h_map);
284 self.tree = Some(tree);
285 self.reference.pos = RawLine::new(min, max).midpoint();
286 if self.map_area.area() == 0.0 {
290 self.reference.dist = 3000.00;
291 } else {
292 let rect = RawLine::new(
293 RawPoint::from(self.map_area.left_top()),
294 RawPoint::from(self.map_area.right_bottom()),
295 );
296 self.reference.dist = rect.distance();
297 }
298 self.current = self.reference.clone();
299 self.calculate_visible_points();
300 }
301
302 pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
303 #[cfg(feature = "puffin")]
304 puffin::profile_scope!("set_pos_from_nodeid");
305 if let Some(hash_map) = &self.points
306 && let Some(map_point) = hash_map.get(&node_id)
307 {
308 self.reference.pos = map_point.raw_point;
309 self.adjust_bounds();
310 self.calculate_visible_points();
311 }
312 }
313
314 pub fn set_pos(&mut self, position: [f32; 2]) {
315 #[cfg(feature = "puffin")]
316 puffin::profile_scope!("set_pos");
317 let point = RawPoint::from(position);
318 self.reference.pos = point;
319 self.adjust_bounds();
320 self.calculate_visible_points();
321 }
322
323 pub fn get_pos(self) -> [f32; 2] {
324 #[cfg(feature = "puffin")]
325 puffin::profile_scope!("get_pos");
326 self.reference.pos.into()
327 }
328
329 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
330 #[cfg(feature = "puffin")]
331 puffin::profile_scope!("add_labels");
332 self.labels = labels;
333 }
334
335 pub fn add_lines(&mut self, lines: HashMap<String, MapLine>) {
336 #[cfg(feature = "puffin")]
337 puffin::profile_scope!("add_lines");
338 self.lines = Some(lines);
339 }
340
341 fn adjust_bounds(&mut self) {
342 #[cfg(feature = "puffin")]
343 puffin::profile_scope!("adjust_bounds");
344 self.current.max = self.reference.max * self.zoom;
345 self.current.min = self.reference.min * self.zoom;
346 self.current.dist = self.reference.dist / self.zoom;
347 self.current.pos = self.reference.pos * self.zoom;
348 }
349
350 fn capture_mouse_events(&mut self, ui: &Ui, resp: &Response) {
351 #[cfg(feature = "puffin")]
352 puffin::profile_scope!("capture_mouse_events");
353 if ui.rect_contains_pointer(self.map_area) {
355 ui.input(|x| {
356 #[cfg(feature = "puffin")]
357 puffin::profile_scope!("capture_mouse_events");
358
359 if !x.events.is_empty() {
360 for event in &x.events {
361 match event {
362 Event::MouseWheel {
363 unit: _,
364 delta,
365 modifiers,
366 phase: _,
367 } => {
368 #[cfg(target_os = "macos")]
369 let zoom_modifier = if modifiers.mac_cmd {
370 delta.y / 80.00
371 } else {
372 delta.y / 400.00
373 };
374
375 #[cfg(not(target_os = "macos"))]
376 let zoom_modifier = if modifiers.ctrl {
377 delta.y / 8.00
378 } else {
379 delta.y / 40.00
380 };
381
382 let mut pre_zoom = self.zoom + zoom_modifier;
383 if pre_zoom > self.settings.max_zoom {
384 pre_zoom = self.settings.max_zoom;
385 }
386 if pre_zoom < self.settings.min_zoom {
387 pre_zoom = self.settings.min_zoom;
388 }
389 self.zoom = pre_zoom;
390 }
391 _ => {
392 continue;
393 }
394 };
395 }
396 }
397 });
398 }
399 if resp.secondary_clicked() {}
400 }
401
402 pub fn set_zoom(&mut self, value: f32) {
403 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
404 self.zoom = value;
405 }
406 }
407
408 pub fn get_zoom(&mut self) -> f32 {
409 self.zoom
410 }
411
412 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
413 let style_index = ui_obj.visuals().dark_mode as usize;
414
415 if self.current_index != style_index {
416 #[cfg(feature = "puffin")]
417 puffin::profile_scope!("asign_visual_style");
418
419 self.current_index = style_index;
420 self.style = ui_obj.style_mut().clone();
421 self.style.visuals.extreme_bg_color =
422 self.settings.styles[style_index].background_color;
423 self.style.visuals.window_stroke = self.settings.styles[style_index].border.unwrap();
424 }
425 }
426
427 fn print_debug_info(&mut self, paint: Painter, resp: Response) {
428 #[cfg(feature = "puffin")]
429 puffin::profile_scope!("printing debug data");
430
431 let mut init_pos = Pos2::new(
432 self.map_area.left_top().x + 10.00,
433 self.map_area.left_top().y + 10.00,
434 );
435 let mut msg = "MIN:".to_string()
436 + self.current.min.components[0].to_string().as_str()
437 + ","
438 + self.current.min.components[1].to_string().as_str();
439 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
440 init_pos.y += 15.0;
441 msg = "MAX:".to_string()
442 + self.current.max.components[0].to_string().as_str()
443 + ","
444 + self.current.max.components[1].to_string().as_str();
445 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
446 init_pos.y += 15.0;
447 msg = "CUR:(".to_string()
448 + self.current.pos.components[0].to_string().as_str()
449 + ","
450 + self.current.pos.components[1].to_string().as_str()
451 + ")";
452 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
453 init_pos.y += 15.0;
454 msg = "DST:".to_string() + self.current.dist.to_string().as_str();
455 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
456 init_pos.y += 15.0;
457 msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
458 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
459 init_pos.y += 15.0;
460 msg = "REC:(".to_string()
461 + self.map_area.left_top().x.to_string().as_str()
462 + ","
463 + self.map_area.left_top().y.to_string().as_str()
464 + "),("
465 + self.map_area.right_bottom().x.to_string().as_str()
466 + ","
467 + self.map_area.right_bottom().y.to_string().as_str()
468 + ")";
469 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
470 if let Some(points) = &self.points {
471 init_pos.y += 15.0;
472 msg = "NUM:".to_string() + points.len().to_string().as_str();
473 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
474 }
475 if !self.visible_points.is_empty() {
476 init_pos.y += 15.0;
477 msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
478 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
479 }
480 if let Some(pointer_pos) = resp.hover_pos() {
481 init_pos.y += 15.0;
482 msg = "HVR:".to_string()
483 + pointer_pos.x.to_string().as_str()
484 + ","
485 + pointer_pos.y.to_string().as_str();
486 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
487 }
488 let vec = resp.drag_delta();
489 if vec.length() != 0.0 {
490 init_pos.y += 15.0;
491 msg = "DRG:".to_string()
492 + vec.to_pos2().x.to_string().as_str()
493 + ","
494 + vec.to_pos2().y.to_string().as_str();
495 paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
496 }
497 }
498
499 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
500 #[cfg(feature = "puffin")]
501 puffin::profile_scope!("map_ui_paint_sub_components");
502 let zoom_slider = egui::Slider::new(
503 &mut self.zoom,
504 self.settings.min_zoom..=self.settings.max_zoom,
505 )
506 .show_value(false)
507 .orientation(SliderOrientation::Vertical);
509 let mut pos1 = rect.right_top();
510 let mut pos2 = rect.right_top();
511 pos1.x -= 80.0;
512 pos1.y += 120.0;
513 pos2.x -= 60.0;
514 pos2.y += 240.0;
515
516 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
518 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
520 ui_obj.scope_builder(ui_builder, |ui_obj| {
522 ui_obj.add(zoom_slider);
523 });
524 }
525
526 fn paint_map_points(
527 &self,
528 vec_points: &Vec<isize>,
529 hashm: &Option<HashMap<usize, MapPoint>>,
530 paint: &Painter,
531 ui_obj: &mut Ui,
532 min_point: &RawPoint,
533 resp: &Response,
534 ) -> Result<Vec<usize>, ()> {
535 let mut nearest_id = None;
536 let mut nodes_to_remove = Vec::new();
537 let mut shape_vec = vec![];
538
539 if hashm.is_none() {
540 return Err(());
541 }
542 if vec_points.is_empty() {
543 return Err(());
544 }
545 if self.settings.node_text_visibility == VisibilitySetting::Hover
547 && resp.hovered()
548 && let Some(point) = resp.hover_pos()
549 {
550 let raw_point = RawPoint::from(point);
551 let hovered_map_point = (*min_point + raw_point) / self.zoom;
552 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
553 &hovered_map_point.components,
554 1,
555 &squared_euclidean,
556 ) {
557 nearest_id = Some(nearest_node.first().unwrap().1);
558 }
559 }
560 let mut text_settings = TextSettings {
562 size: 12.00 * self.zoom,
563 anchor: Align2::LEFT_BOTTOM,
564 family: FontFamily::Proportional,
565 text: String::new(),
566 position: RawPoint::default(),
567 text_color: ui_obj.visuals().text_color(),
568 };
569
570 for temp_point in vec_points {
572 let parsed_point = temp_point.cast_unsigned();
573 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
574 #[cfg(feature = "puffin")]
575 puffin::profile_scope!("painting_points_m");
576 let viewport_point = system.raw_point * self.zoom - min_point;
577 if let Some(node_template) = &self.node_template {
578 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
579 node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
580 }
581 } else if self.zoom > self.settings.label_visible_zoom
582 && self.settings.node_text_visibility == VisibilitySetting::Allways
583 || (self.settings.node_text_visibility == VisibilitySetting::Hover
584 && nearest_id.unwrap_or(&0usize) == &system.get_id())
585 {
586 let mut viewport_text = viewport_point;
587 viewport_text.components[0] += 3.0 * self.zoom;
588 viewport_text.components[1] -= 3.0 * self.zoom;
589 text_settings.position = viewport_text;
590 text_settings.text = system.get_name();
591 self.paint_label(paint, &text_settings);
592 }
593
594 let system_id = system.get_id();
595 if let Some(init_time) = self.entities.get(&system_id) {
596 if let Some(template) = &self.node_template {
597 template.notification_ui(
598 ui_obj,
599 viewport_point.into(),
600 self.zoom,
601 *init_time,
602 self.settings.styles[self.current_index].alert_color,
603 );
604 } else {
605 match Animation::pulse(
606 paint,
607 viewport_point,
608 self.zoom,
609 *init_time,
610 self.settings.styles[self.current_index].alert_color,
611 ) {
612 Ok(true) => {
613 ui_obj.ctx().request_repaint();
614 }
615 Ok(false) => nodes_to_remove.push(system_id),
616 Err(_) => (),
617 }
618 }
619 }
620 if let Some(node_template) = &self.node_template {
621 node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
622 } else {
623 shape_vec.push(Shape::circle_filled(
624 viewport_point.into(),
625 4.00 * self.zoom,
626 self.settings.styles[self.current_index].fill_color,
627 ));
628 }
629 }
630 }
631 paint.extend(shape_vec);
632 Ok(nodes_to_remove)
633 }
634
635 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
636 #[cfg(feature = "puffin")]
637 puffin::profile_scope!("paint_map_lines");
638
639 if self.zoom > self.settings.line_visible_zoom {
641 let mut shape_vec = vec![];
642 let mut stroke = self.settings.styles[self.current_index].line.unwrap();
643 let transparency_range = self.zoom - self.settings.line_visible_zoom;
644 if (0.00..0.80).contains(&transparency_range) {
645 let mut tup_stroke = self.settings.styles[self.current_index]
646 .line
647 .unwrap()
648 .color
649 .to_tuple();
650 let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
651 tup_stroke.3 = (255.0 * transparency).round() as u8;
652 let color = Color32::from_rgba_unmultiplied(
653 tup_stroke.0,
654 tup_stroke.1,
655 tup_stroke.2,
656 tup_stroke.3,
657 );
658 stroke = Stroke::new(
659 self.settings.styles[self.current_index].line.unwrap().width,
660 color,
661 );
662 }
663 for line in &self.visible_lines {
665 if let Some(connection) = self.lines.as_ref().unwrap().get(line) {
666 let pos_a = connection.raw_line.points[0] * self.zoom - min_point;
667 let pos_b = connection.raw_line.points[1] * self.zoom - min_point;
668 shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
671 }
673 }
674 painter.extend(shape_vec);
675 }
676 }
677
678 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
679 #[cfg(feature = "puffin")]
680 puffin::profile_scope!("paint_label");
681 paint.text(
682 text_settings.position.into(),
683 text_settings.anchor,
684 text_settings.text.clone(),
685 FontId::new(text_settings.size, text_settings.family.clone()),
686 text_settings.text_color,
687 );
688 }
689
690 pub fn notify(&mut self, id_node: usize, time: Instant) -> Result<bool, Error> {
691 #[cfg(feature = "puffin")]
692 puffin::profile_scope!("notify");
693 self.entities
694 .entry(id_node)
695 .and_modify(|value| *value = time)
696 .or_insert(time);
697 Ok(true)
698 }
699
700 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
701 self.menu_manager = Some(manager);
702 }
703
704 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
705 self.node_template = Some(template);
706 }
707
708 pub fn update_marker(&mut self, id: usize, node_id: usize) {
709 self.markers
710 .entry(id)
711 .and_modify(|value| *value = node_id)
712 .or_insert(node_id);
713 }
714
715 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
716 self.min_size = (width, height);
717 }
718
719 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
720 self.max_size = (width, height);
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use std::time::Duration;
728
729 fn sample_points() -> HashMap<usize, MapPoint> {
730 let mut map = HashMap::new();
731 map.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
732 map.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
733 map.insert(3, MapPoint::new(3, RawPoint::new(-10.0, -10.0)));
734 map
735 }
736
737 #[test]
740 fn map_new_initial_state() {
741 let map = Map::new();
742 assert_eq!(map.zoom, 1.0);
743 assert_eq!(map.previous_zoom, 1.0);
744 assert!(map.points.is_none());
745 assert!(map.lines.is_none());
746 assert!(map.tree.is_none());
747 assert!(map.labels.is_empty());
748 assert!(map.visible_points.is_empty());
749 assert!(map.visible_lines.is_empty());
750 assert!(map.markers.is_empty());
751 assert!(map.entities.is_empty());
752 assert_eq!(map.min_size, (None, None));
753 assert_eq!(map.max_size, (None, None));
754 assert_eq!(map.current_index, 0);
755 }
756
757 #[test]
758 fn map_default_equals_new() {
759 let map = Map::default();
760 assert_eq!(map.zoom, 1.0);
761 assert!(map.points.is_none());
762 }
763
764 #[test]
767 fn set_zoom_within_range() {
768 let mut map = Map::new();
769 map.set_zoom(1.5);
770 assert_eq!(map.get_zoom(), 1.5);
771 }
772
773 #[test]
774 fn set_zoom_at_exact_limits() {
775 let mut map = Map::new();
776 map.set_zoom(map.settings.min_zoom);
777 assert_eq!(map.get_zoom(), 0.1);
778 map.set_zoom(map.settings.max_zoom);
779 assert_eq!(map.get_zoom(), 2.0);
780 }
781
782 #[test]
783 fn set_zoom_out_of_range_is_ignored() {
784 let mut map = Map::new();
785 let initial = map.get_zoom();
786 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
788 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
790 }
791
792 #[test]
795 fn add_hashmap_points_computes_bounds() {
796 let mut map = Map::new();
797 map.add_hashmap_points(sample_points());
798
799 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
800 assert_eq!(map.reference.max.components, [10.0, 10.0]);
801 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
803 assert_eq!(map.reference.dist, 3000.0);
805 assert_eq!(map.current.min.components, map.reference.min.components);
807 assert_eq!(map.current.max.components, map.reference.max.components);
808 assert_eq!(map.current.pos.components, map.reference.pos.components);
809 assert_eq!(map.current.dist, map.reference.dist);
810 assert!(map.points.is_some());
811 assert!(map.tree.is_some());
812 assert_eq!(map.points.as_ref().unwrap().len(), 3);
813 }
814
815 #[test]
816 fn add_hashmap_points_populates_visible_points() {
817 let mut map = Map::new();
818 map.add_hashmap_points(sample_points());
819 assert_eq!(map.visible_points.len(), 3);
821 }
822
823 #[test]
824 fn add_hashmap_points_populates_visible_lines() {
825 let mut points = sample_points();
826 points
827 .get_mut(&1)
828 .unwrap()
829 .connections
830 .push("1-2".to_string());
831 let mut map = Map::new();
832 map.add_hashmap_points(points);
833 assert!(map.visible_lines.contains("1-2"));
834 }
835
836 #[test]
839 fn set_pos_and_get_pos_roundtrip() {
840 let mut map = Map::new();
841 map.set_pos([25.0, -35.0]);
842 assert_eq!(map.get_pos(), [25.0, -35.0]);
843 }
844
845 #[test]
846 fn set_pos_from_nodeid_with_valid_id() {
847 let mut map = Map::new();
848 map.add_hashmap_points(sample_points());
849 map.set_pos_from_nodeid(2);
850 assert_eq!(map.get_pos(), [10.0, 10.0]);
851 }
852
853 #[test]
854 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
855 let mut map = Map::new();
856 map.add_hashmap_points(sample_points());
857 let before = map.reference.pos.components;
858 map.set_pos_from_nodeid(999);
859 assert_eq!(map.reference.pos.components, before);
860 }
861
862 #[test]
863 fn set_pos_from_nodeid_without_points_does_nothing() {
864 let mut map = Map::new();
865 map.set_pos_from_nodeid(1);
866 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
867 }
868
869 #[test]
872 fn add_labels_stores_labels() {
873 let mut map = Map::new();
874 let label = MapLabel {
875 text: "Region".to_string(),
876 center: Pos2::new(1.0, 2.0),
877 };
878 map.add_labels(vec![label]);
879 assert_eq!(map.labels.len(), 1);
880 assert_eq!(map.labels[0].text, "Region");
881 }
882
883 #[test]
884 fn add_lines_stores_lines() {
885 let mut map = Map::new();
886 let mut lines = HashMap::new();
887 lines.insert(
888 "a-b".to_string(),
889 MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(1.0, 1.0)),
890 );
891 map.add_lines(lines);
892 assert!(map.lines.as_ref().unwrap().contains_key("a-b"));
893 }
894
895 #[test]
898 fn notify_inserts_and_updates_entities() {
899 let mut map = Map::new();
900 let t1 = Instant::now();
901 assert!(map.notify(5, t1).is_ok());
902 assert_eq!(map.entities.get(&5), Some(&t1));
903
904 let t2 = t1 + Duration::from_secs(1);
905 assert!(map.notify(5, t2).is_ok());
906 assert_eq!(map.entities.get(&5), Some(&t2));
907 assert_eq!(map.entities.len(), 1);
908 }
909
910 #[test]
911 fn update_marker_inserts_and_updates() {
912 let mut map = Map::new();
913 map.update_marker(1, 100);
914 assert_eq!(map.markers.get(&1), Some(&100));
915 map.update_marker(1, 200);
916 assert_eq!(map.markers.get(&1), Some(&200));
917 assert_eq!(map.markers.len(), 1);
918 }
919
920 #[test]
923 fn allocate_at_least_sets_min_size() {
924 let mut map = Map::new();
925 map.allocate_at_least(Some(100.0), None);
926 assert_eq!(map.min_size, (Some(100.0), None));
927 }
928
929 #[test]
930 fn allocate_at_most_sets_max_size() {
931 let mut map = Map::new();
932 map.allocate_at_most(None, Some(200.0));
933 assert_eq!(map.max_size, (None, Some(200.0)));
934 }
935
936 #[test]
939 fn adjust_bounds_scales_with_zoom() {
940 let mut map = Map::new();
941 map.reference.min = RawPoint::new(-10.0, -20.0);
942 map.reference.max = RawPoint::new(10.0, 20.0);
943 map.reference.pos = RawPoint::new(5.0, 5.0);
944 map.reference.dist = 100.0;
945 map.set_zoom(2.0);
946 map.adjust_bounds();
947
948 assert_eq!(map.current.max.components, [20.0, 40.0]);
949 assert_eq!(map.current.min.components, [-20.0, -40.0]);
950 assert_eq!(map.current.pos.components, [10.0, 10.0]);
951 assert_eq!(map.current.dist, 50.0);
952 }
953}