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