Skip to main content

egui_map/
map.rs

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// This can by any object or point with its associated metadata
20/// Struct that contains coordinates to help calculate nearest point in space
21
22#[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        // we define the initial coordinate as the center of such rectangle
57        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                    // filling text settings
82                    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                // Here we determine the widget center to print all nodes
104                // let min_point = self.current.pos - RawPoint::try_from([self.map_area.center().x,self.map_area.center().y]).unwrap();
105
106                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        // We stablish the max and min coordinates in this map, this wont change until we change the point hash map
281        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        // we create a rect that include every node in the map
287        // Stupid fix because rect area could be infinite
288        // I need to implement a more elegant fix
289        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        // capture MouseWheel Event for Zoom control change
354        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        //.step_by(0.1)
508        .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        // TODO: Verify if this implementation its correct migrated from allocate_ui_at_rect()
517        let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
518        //ui_obj.allocate_ui_with_layout(sub_rect.size(), egui::Layout::right_to_left(Align::TOP), |ui_obj| {
519        let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
520        //});
521        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        // detecting the nearest hover node
546        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        // filling text settings
561        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        // Drawing Points
571        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        // Drawing Lines
640        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            //let stroke = Stroke::new(10.0,Color32::GREEN);
664            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                    //let pos_a = connection.raw_line.points[0] / self.zoom - min_point;
669                    //let pos_b = connection.raw_line.points[1] / self.zoom - min_point;
670                    shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
671                    //shape_vec.push(painter.line_segment([pos_a.into(),pos_b.into()], stroke));
672                }
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    // ---------- construcción ----------
738
739    #[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    // ---------- zoom ----------
765
766    #[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); // por debajo de min_zoom
787        assert_eq!(map.get_zoom(), initial);
788        map.set_zoom(2.5); // por encima de max_zoom
789        assert_eq!(map.get_zoom(), initial);
790    }
791
792    // ---------- puntos ----------
793
794    #[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        // pos es el punto medio del rectángulo que contiene todos los puntos
802        assert_eq!(map.reference.pos.components, [0.0, 0.0]);
803        // map_area tiene área 0 antes de renderizar, así que dist es el valor fijo
804        assert_eq!(map.reference.dist, 3000.0);
805        // current se inicializa como copia de reference
806        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        // todos los puntos de muestra caen dentro del radio por defecto
820        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    // ---------- posición ----------
837
838    #[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    // ---------- etiquetas y líneas ----------
870
871    #[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    // ---------- notificaciones y marcadores ----------
896
897    #[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    // ---------- tamaño ----------
921
922    #[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    // ---------- bounds ----------
937
938    #[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}