Skip to main content

egui_map/
map.rs

1//! Interactive map widget and the data types it renders.
2//!
3//! [`Map`] is an [`egui::Widget`] that draws a 2D set of nodes
4//! ([`objects::MapPoint`]), the connection lines between them
5//! ([`objects::MapLine`]) and free-floating text labels
6//! ([`objects::MapLabel`]). Nodes are indexed in a kd-tree so that only the
7//! ones inside the current viewport are painted each frame.
8//!
9//! ## Coordinate model
10//!
11//! The widget works with two coordinate spaces:
12//!
13//! - **Map coordinates**: the logical position of your nodes, as loaded through
14//!   [`Map::add_hashmap_points`].
15//! - **Screen coordinates**: positions inside the widget's rectangle on screen.
16//!
17//! Both are related by the current zoom factor and viewport origin:
18//! `screen = map * zoom - origin`. Use [`Map::set_zoom`], [`Map::set_pos`] and
19//! [`Map::set_pos_from_nodeid`] to control the visible region.
20//!
21//! ## Connecting nodes with lines
22//!
23//! Lines are wired up in three steps:
24//!
25//! 1. Create the nodes as a [`HashMap`] keyed by node id.
26//! 2. For every connection, choose a unique string id and push it into
27//!    [`MapPoint::connections`] of **both** endpoint nodes.
28//! 3. Load the nodes with [`Map::add_hashmap_points`], then load a
29//!    [`HashMap`] of [`MapLine`] keyed by those same connection ids with
30//!    [`Map::add_lines`].
31//!
32//! ```
33//! use egui_map::map::Map;
34//! use egui_map::map::objects::{MapLine, MapPoint, RawPoint};
35//! use std::collections::HashMap;
36//!
37//! // 1. Create the nodes.
38//! let mut points: HashMap<usize, MapPoint> = HashMap::new();
39//! points.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
40//! points.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
41//!
42//! // 2. Register the connection id on both endpoints.
43//! for id in [1, 2] {
44//!     points
45//!         .get_mut(&id)
46//!         .unwrap()
47//!         .connections
48//!         .push("1-2".to_string());
49//! }
50//!
51//! let mut map = Map::new();
52//! map.add_hashmap_points(points);
53//!
54//! // 3. Provide the line geometry keyed by the same connection id.
55//! let mut lines: HashMap<String, MapLine> = HashMap::new();
56//! let mut line = MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0));
57//! line.id = Some("1-2".to_string());
58//! lines.insert("1-2".to_string(), line);
59//! map.add_lines(lines);
60//! ```
61//!
62//! A line is only drawn while the zoom level is above
63//! [`MapSettings::line_visible_zoom`] and at least one of its endpoints is
64//! inside the viewport.
65//!
66//! ## Custom node rendering
67//!
68//! Install a [`NodeTemplate`] implementation with [`Map::set_node_template`]
69//! to take over the rendering of nodes, selection highlights, notification
70//! animations and markers. Note that this replaces
71//! *all* built-in node rendering, including the node name labels: draw them
72//! yourself in [`NodeTemplate::node_ui`] if you need them.
73
74use crate::map::animation::Animation;
75use crate::map::objects::{
76    ContextMenuManager, MapBounds, MapLabel, MapLine, MapPoint, MapSettings, MapStyle, RawLine,
77    RawPoint, TextSettings, VisibilitySetting,
78};
79use egui::{epaint::CircleShape, widgets::*, *};
80use kdtree::KdTree;
81use kdtree::distance::squared_euclidean;
82use std::collections::{HashMap, HashSet};
83use std::rc::Rc;
84use std::time::Instant;
85
86use self::objects::NodeTemplate;
87
88pub mod animation;
89pub mod objects;
90
91/// An interactive 2D map widget.
92///
93/// `Map` renders a set of nodes ([`objects::MapPoint`]), connection lines
94/// ([`objects::MapLine`]) and text labels ([`objects::MapLabel`]). The user can
95/// pan the view by dragging and zoom with the mouse wheel (hold `Ctrl` — or
96/// `Cmd` on macOS — to zoom faster), or use the built-in zoom slider drawn at
97/// the top-right corner of the widget.
98///
99/// The map is fed through [`Map::add_hashmap_points`], which also builds the
100/// internal kd-tree used for viewport culling and nearest-node hover queries.
101/// Behavior and appearance are configured through the public
102/// [`settings`](Map::settings) field (see [`objects::MapSettings`]).
103///
104/// Rendering of nodes and their visual effects (selection highlight,
105/// notifications and markers) can be fully customized by installing a
106/// [`objects::NodeTemplate`] implementation with [`Map::set_node_template`];
107/// likewise, a right-click context menu can be provided with
108/// [`Map::set_context_manager`].
109///
110/// # Examples
111///
112/// ```no_run
113/// # fn example(ui: &mut egui::Ui) {
114/// use egui_map::map::Map;
115/// use egui_map::map::objects::{MapPoint, RawPoint};
116/// use std::collections::HashMap;
117///
118/// let mut points = HashMap::new();
119/// points.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
120///
121/// let mut map = Map::new();
122/// map.add_hashmap_points(points);
123///
124/// // Every frame, inside your egui update logic:
125/// ui.add(&mut map);
126/// # }
127/// ```
128#[derive(Clone)]
129pub struct Map {
130    zoom: f32,
131    previous_zoom: f32,
132    points: Option<HashMap<usize, MapPoint>>,
133    lines: Option<HashMap<Rc<str>, MapLine>>,
134    labels: Vec<MapLabel>,
135    tree: Option<KdTree<f32, usize, [f32; 2]>>,
136    visible_points: Vec<isize>,
137    map_area: Rect,
138    reference: MapBounds,
139    current: MapBounds,
140    current_index: usize,
141    entities: HashMap<usize, Instant>,
142    min_size: (Option<f32>, Option<f32>),
143    max_size: (Option<f32>, Option<f32>),
144    /// Behavior and appearance configuration (zoom limits, visibility
145    /// thresholds and per-theme styles). See [`objects::MapSettings`].
146    pub settings: MapSettings,
147    menu_manager: Option<Rc<dyn ContextMenuManager>>,
148    node_template: Option<Rc<dyn NodeTemplate>>,
149    visible_lines: HashSet<Rc<str>>,
150    markers: HashMap<usize, usize>,
151}
152
153impl Default for Map {
154    /// Creates an empty map; equivalent to [`Map::new`].
155    fn default() -> Self {
156        Map::new()
157    }
158}
159
160impl Widget for &mut Map {
161    /// Renders the map, handling panning (drag), zooming (mouse wheel) and the
162    /// right-click context menu if one was installed.
163    fn ui(self, ui: &mut egui::Ui) -> Response {
164        let rect = self.calculate_widget_dimensions(ui);
165
166        // we define the initial coordinate as the center of such rectangle
167        self.reference.dist = rect.distance();
168
169        self.assign_visual_style(ui);
170
171        let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
172
173        let inner_response = canvas.show(ui, |ui| {
174            #[cfg(feature = "puffin")]
175            puffin::profile_scope!("paint_map");
176
177            if ui.is_rect_visible(self.map_area) {
178                let (resp, paint) =
179                    ui.allocate_painter(self.map_area.size(), egui::Sense::click_and_drag());
180                let vec = resp.drag_delta();
181                if vec.length() != 0.0 {
182                    #[cfg(feature = "puffin")]
183                    puffin::profile_scope!("calculating_points_in_visible_area");
184
185                    let coords = RawPoint::from(vec.to_pos2());
186                    let new_pos = self.reference.pos - (coords / self.zoom);
187                    self.set_pos(new_pos.into());
188                }
189                if self.zoom < self.settings.line_visible_zoom {
190                    // filling text settings
191                    let mut text_settings = TextSettings {
192                        size: 12.00 * self.zoom * 2.00,
193                        anchor: Align2::CENTER_CENTER,
194                        family: FontFamily::Proportional,
195                        text: String::new(),
196                        position: RawPoint::default(),
197                        text_color: ui.visuals().text_color(),
198                    };
199                    for label in &self.labels {
200                        text_settings.text.clone_from(&label.text);
201                        text_settings.position = RawPoint::from(label.center);
202                        self.paint_label(&paint, &text_settings);
203                    }
204                }
205
206                let rect_midpoint = RawPoint::from(self.map_area.center());
207                let min_point = self.current.pos - rect_midpoint;
208                let vec_points = &self.visible_points;
209                let hashm = &self.points;
210
211                // Safety net: drop stale notifications even if their node is
212                // outside the viewport and never finishes its animation.
213                let now = Instant::now();
214                self.entities
215                    .retain(|_, init| now.duration_since(*init).as_secs_f32() < 10.0);
216
217                self.paint_map_lines(&paint, &min_point);
218
219                if let Ok(nodes_to_remove) =
220                    self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
221                {
222                    for node in nodes_to_remove {
223                        self.entities.remove(&node);
224                    }
225                }
226
227                for marker in &self.markers {
228                    if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
229                        let adjusted_point = point.raw_point * self.zoom - min_point;
230                        if let Some(template) = &self.node_template {
231                            template.marker_ui(ui, adjusted_point.into(), self.zoom);
232                        } else {
233                            let mut shapes = Vec::new();
234                            let color = if ui.visuals().dark_mode {
235                                Color32::LIGHT_GREEN
236                            } else {
237                                Color32::GREEN
238                            };
239                            let millis = std::time::SystemTime::now()
240                                .duration_since(std::time::UNIX_EPOCH)
241                                .unwrap_or_default()
242                                .as_millis();
243                            let mut transparency = (millis % 2550 / 5) as i64;
244                            if transparency > 255 {
245                                transparency = 255 - (transparency - 255)
246                            }
247                            let corrected_color = Color32::from_rgba_unmultiplied(
248                                color.r(),
249                                color.g(),
250                                color.b(),
251                                transparency as u8,
252                            );
253                            shapes.push(Shape::Circle(CircleShape::stroke(
254                                adjusted_point.into(),
255                                4.0 * self.zoom,
256                                Stroke::new(9.0 * self.zoom, corrected_color),
257                            )));
258                            ui.ctx().request_repaint();
259                            ui.painter().extend(shapes);
260                        }
261                    }
262                }
263
264                self.paint_sub_components(ui, self.map_area);
265
266                self.capture_mouse_events(ui, &resp);
267
268                if self.zoom != self.previous_zoom {
269                    #[cfg(feature = "puffin")]
270                    puffin::profile_scope!("calculating viewport with zoom");
271                    self.adjust_bounds();
272                    self.calculate_visible_points();
273                    self.previous_zoom = self.zoom;
274                }
275
276                if let Some(menu_mon) = &mut self.menu_manager {
277                    resp.context_menu(|ui| {
278                        menu_mon.ui(ui);
279                    });
280                }
281
282                if cfg!(debug_assertions) {
283                    self.print_debug_info(paint, resp);
284                }
285            }
286        });
287        ui.allocate_space(self.map_area.size());
288        inner_response.response
289    }
290}
291
292impl Map {
293    /// Creates an empty map widget with default [`MapSettings`].
294    ///
295    /// The widget displays nothing until nodes are loaded with
296    /// [`Map::add_hashmap_points`].
297    pub fn new() -> Self {
298        let settings = MapSettings::default();
299        Self {
300            zoom: 1.0,
301            previous_zoom: 1.0,
302            map_area: Rect::NOTHING,
303            tree: None,
304            points: None,
305            lines: None,
306            labels: Vec::new(),
307            visible_points: Vec::new(),
308            current: MapBounds::default(),
309            reference: MapBounds::default(),
310            settings,
311            min_size: (None, None),
312            max_size: (None, None),
313            current_index: 0,
314            entities: HashMap::new(),
315            menu_manager: None,
316            node_template: None,
317            visible_lines: HashSet::new(),
318            markers: HashMap::new(),
319        }
320    }
321
322    fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
323        let available = ui.available_rect_before_wrap();
324        let mut size = available.size();
325        if let Some(max_width) = self.max_size.0 {
326            size.x = size.x.min(max_width);
327        }
328        if let Some(max_height) = self.max_size.1 {
329            size.y = size.y.min(max_height);
330        }
331        if let Some(min_width) = self.min_size.0 {
332            size.x = size.x.max(min_width);
333        }
334        if let Some(min_height) = self.min_size.1 {
335            size.y = size.y.max(min_height);
336        }
337        self.map_area = Rect::from_min_size(available.min, size);
338        RawLine::new(
339            RawPoint::from(self.map_area.left_top()),
340            RawPoint::from(self.map_area.right_bottom()),
341        )
342    }
343
344    fn calculate_visible_points(&mut self) {
345        #[cfg(feature = "puffin")]
346        puffin::profile_scope!("calculate_visible_points");
347        if self.current.dist > 0.0
348            && self.current.dist < f32::INFINITY
349            && let Some(tree) = &self.tree
350        {
351            let center = self.current.pos / self.zoom;
352            let radius = self.current.dist.powi(2);
353            let point: [f32; 2] = center.into();
354            let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
355            self.visible_points.clear();
356            self.visible_lines.clear();
357            for point in vis_pos {
358                self.visible_points.push(point.1.cast_signed());
359                let system = self.points.as_ref().unwrap().get(point.1);
360                for connection in &system.unwrap().connections {
361                    // Reuse the Rc key stored in the lines map instead of
362                    // allocating a new String per connection.
363                    if let Some((key, _)) = self
364                        .lines
365                        .as_ref()
366                        .and_then(|lines| lines.get_key_value(connection.as_str()))
367                    {
368                        self.visible_lines.insert(Rc::clone(key));
369                    }
370                }
371            }
372        }
373    }
374
375    /// Loads the node set and (re)builds the spatial index.
376    ///
377    /// This replaces any previously loaded points, computes the bounding box of
378    /// the whole set, centers the view on its midpoint and refreshes the list
379    /// of visible nodes. It must be called at least once before the widget can
380    /// display anything.
381    ///
382    /// The kd-tree built here is what enables viewport culling and
383    /// nearest-neighbor hover lookups, so calling this method on every frame is
384    /// discouraged; call it only when the node set changes.
385    ///
386    /// # Examples
387    ///
388    /// ```
389    /// use egui_map::map::Map;
390    /// use egui_map::map::objects::{MapPoint, RawPoint};
391    /// use std::collections::HashMap;
392    ///
393    /// let mut points = HashMap::new();
394    /// points.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
395    /// points.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
396    ///
397    /// let mut map = Map::new();
398    /// map.add_hashmap_points(points);
399    ///
400    /// // The view is centered on the midpoint of the loaded nodes.
401    /// assert_eq!(map.get_pos(), [5.0, 5.0]);
402    /// ```
403    pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
404        #[cfg(feature = "puffin")]
405        puffin::profile_scope!("add_hashmap_points");
406        let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
407        let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
408        let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
409
410        for entry in hash_map.iter() {
411            for i in 0..min.components.len() {
412                if entry.1.raw_point.components[i] < min.components[i] {
413                    min.components[i] = entry.1.raw_point.components[i];
414                }
415                if entry.1.raw_point.components[i] > max.components[i] {
416                    max.components[i] = entry.1.raw_point.components[i];
417                }
418            }
419            let _result = tree.add(entry.1.raw_point.into(), *entry.0);
420        }
421
422        // We stablish the max and min coordinates in this map, this wont change until we change the point hash map
423        self.reference.min = min;
424        self.reference.max = max;
425        self.points = Some(hash_map);
426        self.tree = Some(tree);
427        self.reference.pos = RawLine::new(min, max).midpoint();
428        // we create a rect that include every node in the map
429        // Stupid fix because rect area could be infinite
430        // I need to implement a more elegant fix
431        if self.map_area.area() == 0.0 {
432            self.reference.dist = 3000.00;
433        } else {
434            let rect = RawLine::new(
435                RawPoint::from(self.map_area.left_top()),
436                RawPoint::from(self.map_area.right_bottom()),
437            );
438            self.reference.dist = rect.distance();
439        }
440        self.current = self.reference.clone();
441        self.calculate_visible_points();
442    }
443
444    /// Centers the view on the node with the given id.
445    ///
446    /// Does nothing if no points have been loaded yet or if `node_id` is
447    /// unknown.
448    pub fn set_pos_from_nodeid(&mut self, node_id: usize) {
449        #[cfg(feature = "puffin")]
450        puffin::profile_scope!("set_pos_from_nodeid");
451        if let Some(hash_map) = &self.points
452            && let Some(map_point) = hash_map.get(&node_id)
453        {
454            self.reference.pos = map_point.raw_point;
455            self.adjust_bounds();
456            self.calculate_visible_points();
457        }
458    }
459
460    /// Centers the view on the given map coordinates.
461    pub fn set_pos(&mut self, position: [f32; 2]) {
462        #[cfg(feature = "puffin")]
463        puffin::profile_scope!("set_pos");
464        let point = RawPoint::from(position);
465        self.reference.pos = point;
466        self.adjust_bounds();
467        self.calculate_visible_points();
468    }
469
470    /// Returns the map coordinates the view is currently centered on.
471    pub fn get_pos(&self) -> [f32; 2] {
472        #[cfg(feature = "puffin")]
473        puffin::profile_scope!("get_pos");
474        self.reference.pos.into()
475    }
476
477    /// Replaces the set of free-floating text labels drawn on the map.
478    ///
479    /// Labels are only rendered while the zoom level is below
480    /// [`MapSettings::line_visible_zoom`].
481    pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
482        #[cfg(feature = "puffin")]
483        puffin::profile_scope!("add_labels");
484        self.labels = labels;
485    }
486
487    /// Replaces the set of connection lines between nodes.
488    ///
489    /// Lines are keyed by a connection id that the endpoint nodes must
490    /// reference through [`MapPoint::connections`] — push each line's key into
491    /// the `connections` of the nodes it joins. A line is only drawn while at
492    /// least one of its endpoints is visible and the zoom level is above
493    /// [`MapSettings::line_visible_zoom`].
494    ///
495    /// See the [module-level example](self#connecting-nodes-with-lines) for
496    /// the complete wiring.
497    pub fn add_lines(&mut self, lines: HashMap<String, MapLine>) {
498        #[cfg(feature = "puffin")]
499        puffin::profile_scope!("add_lines");
500        // Intern the keys as Rc<str> so visible_lines can share them without
501        // allocating new Strings on every viewport recalculation.
502        self.lines = Some(
503            lines
504                .into_iter()
505                .map(|(key, line)| (Rc::from(key.as_str()), line))
506                .collect(),
507        );
508        // Refresh visible_lines so lines loaded *after* the points (the
509        // natural order in the examples) are drawn from the first frame,
510        // without needing a pan/zoom to trigger the recalculation. Safe to
511        // call before add_hashmap_points: it early-returns while the spatial
512        // index does not exist yet.
513        self.calculate_visible_points();
514    }
515
516    fn adjust_bounds(&mut self) {
517        #[cfg(feature = "puffin")]
518        puffin::profile_scope!("adjust_bounds");
519        self.current.max = self.reference.max * self.zoom;
520        self.current.min = self.reference.min * self.zoom;
521        self.current.dist = self.reference.dist / self.zoom;
522        self.current.pos = self.reference.pos * self.zoom;
523    }
524
525    fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
526        #[cfg(feature = "puffin")]
527        puffin::profile_scope!("capture_mouse_events");
528        // capture MouseWheel Event for Zoom control change
529        if ui.rect_contains_pointer(self.map_area) {
530            ui.input(|x| {
531                #[cfg(feature = "puffin")]
532                puffin::profile_scope!("capture_mouse_events");
533
534                if !x.events.is_empty() {
535                    for event in &x.events {
536                        match event {
537                            Event::MouseWheel {
538                                unit: _,
539                                delta,
540                                modifiers,
541                                phase: _,
542                            } => {
543                                #[cfg(target_os = "macos")]
544                                let zoom_modifier = if modifiers.mac_cmd {
545                                    delta.y / 80.00
546                                } else {
547                                    delta.y / 400.00
548                                };
549
550                                #[cfg(not(target_os = "macos"))]
551                                let zoom_modifier = if modifiers.ctrl {
552                                    delta.y / 8.00
553                                } else {
554                                    delta.y / 40.00
555                                };
556
557                                let mut pre_zoom = self.zoom + zoom_modifier;
558                                if pre_zoom > self.settings.max_zoom {
559                                    pre_zoom = self.settings.max_zoom;
560                                }
561                                if pre_zoom < self.settings.min_zoom {
562                                    pre_zoom = self.settings.min_zoom;
563                                }
564                                self.zoom = pre_zoom;
565                            }
566                            _ => {
567                                continue;
568                            }
569                        };
570                    }
571                }
572            });
573        }
574    }
575
576    /// Sets the zoom factor.
577    ///
578    /// Values outside the [`MapSettings::min_zoom`]..=[`MapSettings::max_zoom`]
579    /// range are ignored.
580    pub fn set_zoom(&mut self, value: f32) {
581        if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
582            self.zoom = value;
583        }
584    }
585
586    /// Returns the current zoom factor.
587    pub fn get_zoom(&mut self) -> f32 {
588        self.zoom
589    }
590
591    /// Returns the style for the current theme, falling back to the first
592    /// style if the current theme index has no entry.
593    fn current_style(&self) -> &MapStyle {
594        self.settings
595            .styles
596            .get(self.current_index)
597            .or(self.settings.styles.first())
598            .expect("MapSettings::styles must not be empty")
599    }
600
601    fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
602        let style_index = ui_obj.visuals().dark_mode as usize;
603
604        if self.current_index != style_index {
605            #[cfg(feature = "puffin")]
606            puffin::profile_scope!("asign_visual_style");
607
608            self.current_index = style_index;
609            let map_style = self.current_style();
610            let visuals = &mut ui_obj.style_mut().visuals;
611            visuals.extreme_bg_color = map_style.background_color;
612            if let Some(border) = map_style.border {
613                visuals.window_stroke = border;
614            }
615        }
616    }
617
618    fn print_debug_info(&mut self, paint: Painter, resp: Response) {
619        #[cfg(feature = "puffin")]
620        puffin::profile_scope!("printing debug data");
621
622        let mut init_pos = Pos2::new(
623            self.map_area.left_top().x + 10.00,
624            self.map_area.left_top().y + 10.00,
625        );
626        let mut msg = "MIN:".to_string()
627            + self.current.min.components[0].to_string().as_str()
628            + ","
629            + self.current.min.components[1].to_string().as_str();
630        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
631        init_pos.y += 15.0;
632        msg = "MAX:".to_string()
633            + self.current.max.components[0].to_string().as_str()
634            + ","
635            + self.current.max.components[1].to_string().as_str();
636        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
637        init_pos.y += 15.0;
638        msg = "CUR:(".to_string()
639            + self.current.pos.components[0].to_string().as_str()
640            + ","
641            + self.current.pos.components[1].to_string().as_str()
642            + ")";
643        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
644        init_pos.y += 15.0;
645        msg = "DST:".to_string() + self.current.dist.to_string().as_str();
646        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
647        init_pos.y += 15.0;
648        msg = "ZOM:".to_string() + self.zoom.to_string().as_str();
649        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GREEN, msg);
650        init_pos.y += 15.0;
651        msg = "REC:(".to_string()
652            + self.map_area.left_top().x.to_string().as_str()
653            + ","
654            + self.map_area.left_top().y.to_string().as_str()
655            + "),("
656            + self.map_area.right_bottom().x.to_string().as_str()
657            + ","
658            + self.map_area.right_bottom().y.to_string().as_str()
659            + ")";
660        paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
661        if let Some(points) = &self.points {
662            init_pos.y += 15.0;
663            msg = "NUM:".to_string() + points.len().to_string().as_str();
664            paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
665        }
666        if !self.visible_points.is_empty() {
667            init_pos.y += 15.0;
668            msg = "VIS:".to_string() + self.visible_points.len().to_string().as_str();
669            paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_GREEN, msg);
670        }
671        if let Some(pointer_pos) = resp.hover_pos() {
672            init_pos.y += 15.0;
673            msg = "HVR:".to_string()
674                + pointer_pos.x.to_string().as_str()
675                + ","
676                + pointer_pos.y.to_string().as_str();
677            paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::LIGHT_BLUE, msg);
678        }
679        let vec = resp.drag_delta();
680        if vec.length() != 0.0 {
681            init_pos.y += 15.0;
682            msg = "DRG:".to_string()
683                + vec.to_pos2().x.to_string().as_str()
684                + ","
685                + vec.to_pos2().y.to_string().as_str();
686            paint.debug_text(init_pos, Align2::LEFT_TOP, Color32::GOLD, msg);
687        }
688    }
689
690    fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
691        #[cfg(feature = "puffin")]
692        puffin::profile_scope!("map_ui_paint_sub_components");
693        let zoom_slider = egui::Slider::new(
694            &mut self.zoom,
695            self.settings.min_zoom..=self.settings.max_zoom,
696        )
697        .show_value(false)
698        .orientation(SliderOrientation::Vertical);
699        let mut pos1 = rect.right_top();
700        let mut pos2 = rect.right_top();
701        pos1.x -= 80.0;
702        pos1.y += 120.0;
703        pos2.x -= 60.0;
704        pos2.y += 240.0;
705
706        let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
707        let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
708        ui_obj.scope_builder(ui_builder, |ui_obj| {
709            ui_obj.add(zoom_slider);
710        });
711    }
712
713    fn paint_map_points(
714        &self,
715        vec_points: &Vec<isize>,
716        hashm: &Option<HashMap<usize, MapPoint>>,
717        paint: &Painter,
718        ui_obj: &mut Ui,
719        min_point: &RawPoint,
720        resp: &Response,
721    ) -> Result<Vec<usize>, ()> {
722        let mut nearest_id = None;
723        let mut nodes_to_remove = Vec::new();
724        let mut shape_vec = vec![];
725
726        if hashm.is_none() {
727            return Err(());
728        }
729        if vec_points.is_empty() {
730            return Err(());
731        }
732        // detecting the nearest hover node
733        if self.settings.node_text_visibility == VisibilitySetting::Hover
734            && resp.hovered()
735            && let Some(point) = resp.hover_pos()
736        {
737            let raw_point = RawPoint::from(point);
738            let hovered_map_point = (*min_point + raw_point) / self.zoom;
739            if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
740                &hovered_map_point.components,
741                1,
742                &squared_euclidean,
743            ) {
744                nearest_id = Some(nearest_node.first().unwrap().1);
745            }
746        }
747        // filling text settings
748        let mut text_settings = TextSettings {
749            size: 12.00 * self.zoom,
750            anchor: Align2::LEFT_BOTTOM,
751            family: FontFamily::Proportional,
752            text: String::new(),
753            position: RawPoint::default(),
754            text_color: ui_obj.visuals().text_color(),
755        };
756
757        // Drawing Points
758        for temp_point in vec_points {
759            let parsed_point = temp_point.cast_unsigned();
760            if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
761                #[cfg(feature = "puffin")]
762                puffin::profile_scope!("painting_points_m");
763                let viewport_point = system.raw_point * self.zoom - min_point;
764                if let Some(node_template) = &self.node_template {
765                    if nearest_id.unwrap_or(&0usize) == &system.get_id() {
766                        node_template.selection_ui(ui_obj, viewport_point.into(), self.zoom);
767                    }
768                } else if self.zoom > self.settings.label_visible_zoom
769                    && self.settings.node_text_visibility == VisibilitySetting::Always
770                    || (self.settings.node_text_visibility == VisibilitySetting::Hover
771                        && nearest_id.unwrap_or(&0usize) == &system.get_id())
772                {
773                    let mut viewport_text = viewport_point;
774                    viewport_text.components[0] += 3.0 * self.zoom;
775                    viewport_text.components[1] -= 3.0 * self.zoom;
776                    text_settings.position = viewport_text;
777                    text_settings.text = system.get_name();
778                    self.paint_label(paint, &text_settings);
779                }
780
781                let system_id = system.get_id();
782                if let Some(init_time) = self.entities.get(&system_id) {
783                    if let Some(template) = &self.node_template {
784                        template.notification_ui(
785                            ui_obj,
786                            viewport_point.into(),
787                            self.zoom,
788                            *init_time,
789                            self.current_style().alert_color,
790                        );
791                    } else if Animation::pulse(
792                        paint,
793                        viewport_point,
794                        self.zoom,
795                        *init_time,
796                        self.current_style().alert_color,
797                    ) {
798                        ui_obj.ctx().request_repaint();
799                    } else {
800                        nodes_to_remove.push(system_id);
801                    }
802                }
803                if let Some(node_template) = &self.node_template {
804                    node_template.node_ui(ui_obj, viewport_point.into(), self.zoom, system);
805                } else {
806                    shape_vec.push(Shape::circle_filled(
807                        viewport_point.into(),
808                        4.00 * self.zoom,
809                        self.current_style().fill_color,
810                    ));
811                }
812            }
813        }
814        paint.extend(shape_vec);
815        Ok(nodes_to_remove)
816    }
817
818    fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) {
819        #[cfg(feature = "puffin")]
820        puffin::profile_scope!("paint_map_lines");
821
822        // Drawing Lines
823        if self.zoom > self.settings.line_visible_zoom
824            && let Some(mut stroke) = self.current_style().line
825        {
826            let mut shape_vec = vec![];
827            let transparency_range = self.zoom - self.settings.line_visible_zoom;
828            if (0.00..0.80).contains(&transparency_range) {
829                let mut tup_stroke = stroke.color.to_tuple();
830                let transparency = (self.zoom - self.settings.line_visible_zoom) / 0.80;
831                tup_stroke.3 = (255.0 * transparency).round() as u8;
832                let color = Color32::from_rgba_unmultiplied(
833                    tup_stroke.0,
834                    tup_stroke.1,
835                    tup_stroke.2,
836                    tup_stroke.3,
837                );
838                stroke = Stroke::new(stroke.width, color);
839            }
840            for line in &self.visible_lines {
841                if let Some(connection) = self.lines.as_ref().unwrap().get(line) {
842                    let pos_a = connection.raw_line.points[0] * self.zoom - min_point;
843                    let pos_b = connection.raw_line.points[1] * self.zoom - min_point;
844                    shape_vec.push(Shape::line_segment([pos_a.into(), pos_b.into()], stroke));
845                }
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    /// Triggers a notification highlight on the node `id_node`.
864    ///
865    /// By default the notification is rendered as a pulsing circle that starts
866    /// at `time` and plays for about 3.5 seconds; calling `notify` again for
867    /// the same node restarts the animation. The effect can be customized with
868    /// [`objects::NodeTemplate::notification_ui`].
869    pub fn notify(&mut self, id_node: usize, time: Instant) {
870        #[cfg(feature = "puffin")]
871        puffin::profile_scope!("notify");
872        self.entities
873            .entry(id_node)
874            .and_modify(|value| *value = time)
875            .or_insert(time);
876    }
877
878    /// Installs a right-click context menu whose contents are built by the
879    /// given [`ContextMenuManager`] implementation.
880    pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
881        self.menu_manager = Some(manager);
882    }
883
884    /// Replaces the built-in node rendering with a custom [`NodeTemplate`]
885    /// implementation.
886    ///
887    /// The template takes over the drawing of nodes, selection highlights,
888    /// notification animations and markers — including the node name labels,
889    /// which the widget no longer draws once a template is installed. See the
890    /// [`NodeTemplate`] examples for custom shapes and animations.
891    pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
892        self.node_template = Some(template);
893    }
894
895    /// Adds the marker `id`, or moves it, so it points to the node `node_id`.
896    ///
897    /// Markers are drawn as a blinking ring around the target node unless a
898    /// custom [`objects::NodeTemplate::marker_ui`] is installed.
899    pub fn update_marker(&mut self, id: usize, node_id: usize) {
900        self.markers
901            .entry(id)
902            .and_modify(|value| *value = node_id)
903            .or_insert(node_id);
904    }
905
906    /// Sets the minimum width and/or height the widget should occupy, in egui
907    /// points. `None` leaves the corresponding dimension unconstrained.
908    pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
909        self.min_size = (width, height);
910    }
911
912    /// Sets the maximum width and/or height the widget should occupy, in egui
913    /// points. `None` leaves the corresponding dimension unconstrained.
914    pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
915        self.max_size = (width, height);
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922    use std::time::Duration;
923
924    fn sample_points() -> HashMap<usize, MapPoint> {
925        let mut map = HashMap::new();
926        map.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
927        map.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
928        map.insert(3, MapPoint::new(3, RawPoint::new(-10.0, -10.0)));
929        map
930    }
931
932    // ---------- construcción ----------
933
934    #[test]
935    fn map_new_initial_state() {
936        let map = Map::new();
937        assert_eq!(map.zoom, 1.0);
938        assert_eq!(map.previous_zoom, 1.0);
939        assert!(map.points.is_none());
940        assert!(map.lines.is_none());
941        assert!(map.tree.is_none());
942        assert!(map.labels.is_empty());
943        assert!(map.visible_points.is_empty());
944        assert!(map.visible_lines.is_empty());
945        assert!(map.markers.is_empty());
946        assert!(map.entities.is_empty());
947        assert_eq!(map.min_size, (None, None));
948        assert_eq!(map.max_size, (None, None));
949        assert_eq!(map.current_index, 0);
950    }
951
952    #[test]
953    fn map_default_equals_new() {
954        let map = Map::default();
955        assert_eq!(map.zoom, 1.0);
956        assert!(map.points.is_none());
957    }
958
959    // ---------- zoom ----------
960
961    #[test]
962    fn set_zoom_within_range() {
963        let mut map = Map::new();
964        map.set_zoom(1.5);
965        assert_eq!(map.get_zoom(), 1.5);
966    }
967
968    #[test]
969    fn set_zoom_at_exact_limits() {
970        let mut map = Map::new();
971        map.set_zoom(map.settings.min_zoom);
972        assert_eq!(map.get_zoom(), 0.1);
973        map.set_zoom(map.settings.max_zoom);
974        assert_eq!(map.get_zoom(), 2.0);
975    }
976
977    #[test]
978    fn set_zoom_out_of_range_is_ignored() {
979        let mut map = Map::new();
980        let initial = map.get_zoom();
981        map.set_zoom(0.05); // por debajo de min_zoom
982        assert_eq!(map.get_zoom(), initial);
983        map.set_zoom(2.5); // por encima de max_zoom
984        assert_eq!(map.get_zoom(), initial);
985    }
986
987    // ---------- puntos ----------
988
989    #[test]
990    fn add_hashmap_points_computes_bounds() {
991        let mut map = Map::new();
992        map.add_hashmap_points(sample_points());
993
994        assert_eq!(map.reference.min.components, [-10.0, -10.0]);
995        assert_eq!(map.reference.max.components, [10.0, 10.0]);
996        // pos es el punto medio del rectángulo que contiene todos los puntos
997        assert_eq!(map.reference.pos.components, [0.0, 0.0]);
998        // map_area tiene área 0 antes de renderizar, así que dist es el valor fijo
999        assert_eq!(map.reference.dist, 3000.0);
1000        // current se inicializa como copia de reference
1001        assert_eq!(map.current.min.components, map.reference.min.components);
1002        assert_eq!(map.current.max.components, map.reference.max.components);
1003        assert_eq!(map.current.pos.components, map.reference.pos.components);
1004        assert_eq!(map.current.dist, map.reference.dist);
1005        assert!(map.points.is_some());
1006        assert!(map.tree.is_some());
1007        assert_eq!(map.points.as_ref().unwrap().len(), 3);
1008    }
1009
1010    #[test]
1011    fn add_hashmap_points_populates_visible_points() {
1012        let mut map = Map::new();
1013        map.add_hashmap_points(sample_points());
1014        // todos los puntos de muestra caen dentro del radio por defecto
1015        assert_eq!(map.visible_points.len(), 3);
1016    }
1017
1018    #[test]
1019    fn add_hashmap_points_populates_visible_lines() {
1020        let mut points = sample_points();
1021        points
1022            .get_mut(&1)
1023            .unwrap()
1024            .connections
1025            .push("1-2".to_string());
1026        let mut lines = HashMap::new();
1027        lines.insert(
1028            "1-2".to_string(),
1029            MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0)),
1030        );
1031        let mut map = Map::new();
1032        map.add_lines(lines);
1033        map.add_hashmap_points(points);
1034        assert!(map.visible_lines.contains("1-2"));
1035    }
1036
1037    #[test]
1038    fn add_lines_after_points_populates_visible_lines() {
1039        // Regression: loading points before lines must still paint lines on
1040        // the first frame (previously visible_lines stayed empty until the
1041        // user panned or zoomed, because add_lines never recalculated it).
1042        let mut points = sample_points();
1043        points
1044            .get_mut(&1)
1045            .unwrap()
1046            .connections
1047            .push("1-2".to_string());
1048        let mut map = Map::new();
1049        map.add_hashmap_points(points);
1050
1051        let mut lines = HashMap::new();
1052        lines.insert(
1053            "1-2".to_string(),
1054            MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0)),
1055        );
1056        map.add_lines(lines);
1057
1058        assert!(map.visible_lines.contains("1-2"));
1059    }
1060
1061    #[test]
1062    fn map_check_line_is_painted_on_first_frame() {
1063        use egui::{Context, RawInput, Shape};
1064
1065        // --- arrange ---
1066        let mut map = Map::new();
1067        map.set_zoom(1.0);
1068
1069        let mut point_a = MapPoint::new(0, RawPoint::new(0.0, 0.0));
1070        point_a.connections.push("a0".to_string());
1071        let mut point_b = MapPoint::new(1, RawPoint::new(50.0, 50.0));
1072        point_b.connections.push("a0".to_string());
1073
1074        let mut lines = HashMap::new();
1075        lines.insert(
1076            "a0".to_string(),
1077            MapLine::new(point_a.raw_point, point_b.raw_point),
1078        );
1079
1080        let mut points = HashMap::new();
1081        points.insert(0usize, point_a);
1082        points.insert(1usize, point_b);
1083        // Load points before lines — the natural order shown in the examples.
1084        map.add_hashmap_points(points);
1085        map.add_lines(lines);
1086
1087        map.set_pos([25.0, 25.0]);
1088
1089        assert!(
1090            map.visible_lines.contains("a0"),
1091            "visible_lines must contain \"a0\" after add_hashmap_points"
1092        );
1093
1094        // --- act: 1st frame (no CentralPanel — run_ui creates the root Ui) ---
1095        let ctx = Context::default();
1096        let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
1097        let input = RawInput {
1098            screen_rect: Some(screen),
1099            ..RawInput::default()
1100        };
1101
1102        let output1 = ctx.run_ui(input.clone(), |ui| {
1103            ui.add(&mut map);
1104        });
1105
1106        let segments1: Vec<[egui::Pos2; 2]> = output1
1107            .shapes
1108            .iter()
1109            .filter_map(|cs| match cs.shape {
1110                Shape::LineSegment { points, .. } => Some(points),
1111                _ => None,
1112            })
1113            .collect();
1114
1115        assert!(
1116            !segments1.is_empty(),
1117            "Frame 1: no LineSegment shapes painted (map lines did not draw)"
1118        );
1119
1120        // Expected projection of (0,0)->(50,50) with zoom=1, center=(25,25),
1121        // viewport 500x500: pos_a = (225, 225), pos_b = (275, 275). Tolerance ±2 px.
1122        let expected_a = egui::pos2(225.0, 225.0);
1123        let expected_b = egui::pos2(275.0, 275.0);
1124        let tolerance = 2.0;
1125        let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
1126            let d_a1 = p1.distance(expected_a);
1127            let d_b1 = p2.distance(expected_b);
1128            let d_a2 = p2.distance(expected_a);
1129            let d_b2 = p1.distance(expected_b);
1130            (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
1131        });
1132        assert!(
1133            found_on_frame1,
1134            "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
1135            segments1
1136        );
1137
1138        // --- act: 2nd frame (unchanged) — detect duplicate-lines regression ---
1139        let output2 = ctx.run_ui(input, |ui| {
1140            ui.add(&mut map);
1141        });
1142
1143        let segments2: Vec<[egui::Pos2; 2]> = output2
1144            .shapes
1145            .iter()
1146            .filter_map(|cs| match cs.shape {
1147                Shape::LineSegment { points, .. } => Some(points),
1148                _ => None,
1149            })
1150            .collect();
1151
1152        assert_eq!(
1153            segments1.len(),
1154            segments2.len(),
1155            "Frame 2: expected {} line segments (no duplication across frames), got {}",
1156            segments1.len(),
1157            segments2.len()
1158        );
1159    }
1160
1161    // ---------- posición ----------
1162
1163    #[test]
1164    fn set_pos_and_get_pos_roundtrip() {
1165        let mut map = Map::new();
1166        map.set_pos([25.0, -35.0]);
1167        assert_eq!(map.get_pos(), [25.0, -35.0]);
1168    }
1169
1170    #[test]
1171    fn set_pos_from_nodeid_with_valid_id() {
1172        let mut map = Map::new();
1173        map.add_hashmap_points(sample_points());
1174        map.set_pos_from_nodeid(2);
1175        assert_eq!(map.get_pos(), [10.0, 10.0]);
1176    }
1177
1178    #[test]
1179    fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
1180        let mut map = Map::new();
1181        map.add_hashmap_points(sample_points());
1182        let before = map.reference.pos.components;
1183        map.set_pos_from_nodeid(999);
1184        assert_eq!(map.reference.pos.components, before);
1185    }
1186
1187    #[test]
1188    fn set_pos_from_nodeid_without_points_does_nothing() {
1189        let mut map = Map::new();
1190        map.set_pos_from_nodeid(1);
1191        assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1192    }
1193
1194    // ---------- etiquetas y líneas ----------
1195
1196    #[test]
1197    fn add_labels_stores_labels() {
1198        let mut map = Map::new();
1199        let label = MapLabel {
1200            text: "Region".to_string(),
1201            center: Pos2::new(1.0, 2.0),
1202        };
1203        map.add_labels(vec![label]);
1204        assert_eq!(map.labels.len(), 1);
1205        assert_eq!(map.labels[0].text, "Region");
1206    }
1207
1208    #[test]
1209    fn add_lines_stores_lines() {
1210        let mut map = Map::new();
1211        let mut lines = HashMap::new();
1212        lines.insert(
1213            "a-b".to_string(),
1214            MapLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(1.0, 1.0)),
1215        );
1216        map.add_lines(lines);
1217        assert!(map.lines.as_ref().unwrap().contains_key("a-b"));
1218    }
1219
1220    // ---------- notificaciones y marcadores ----------
1221
1222    #[test]
1223    fn notify_inserts_and_updates_entities() {
1224        let mut map = Map::new();
1225        let t1 = Instant::now();
1226        map.notify(5, t1);
1227        assert_eq!(map.entities.get(&5), Some(&t1));
1228
1229        let t2 = t1 + Duration::from_secs(1);
1230        map.notify(5, t2);
1231        assert_eq!(map.entities.get(&5), Some(&t2));
1232        assert_eq!(map.entities.len(), 1);
1233    }
1234
1235    #[test]
1236    fn update_marker_inserts_and_updates() {
1237        let mut map = Map::new();
1238        map.update_marker(1, 100);
1239        assert_eq!(map.markers.get(&1), Some(&100));
1240        map.update_marker(1, 200);
1241        assert_eq!(map.markers.get(&1), Some(&200));
1242        assert_eq!(map.markers.len(), 1);
1243    }
1244
1245    // ---------- tamaño ----------
1246
1247    #[test]
1248    fn allocate_at_least_sets_min_size() {
1249        let mut map = Map::new();
1250        map.allocate_at_least(Some(100.0), None);
1251        assert_eq!(map.min_size, (Some(100.0), None));
1252    }
1253
1254    #[test]
1255    fn allocate_at_most_sets_max_size() {
1256        let mut map = Map::new();
1257        map.allocate_at_most(None, Some(200.0));
1258        assert_eq!(map.max_size, (None, Some(200.0)));
1259    }
1260
1261    // ---------- bounds ----------
1262
1263    #[test]
1264    fn adjust_bounds_scales_with_zoom() {
1265        let mut map = Map::new();
1266        map.reference.min = RawPoint::new(-10.0, -20.0);
1267        map.reference.max = RawPoint::new(10.0, 20.0);
1268        map.reference.pos = RawPoint::new(5.0, 5.0);
1269        map.reference.dist = 100.0;
1270        map.set_zoom(2.0);
1271        map.adjust_bounds();
1272
1273        assert_eq!(map.current.max.components, [20.0, 40.0]);
1274        assert_eq!(map.current.min.components, [-20.0, -40.0]);
1275        assert_eq!(map.current.pos.components, [10.0, 10.0]);
1276        assert_eq!(map.current.dist, 50.0);
1277    }
1278}