Skip to main content

egui_map/map/
objects.rs

1//! Data types consumed by the [`Map`](super::Map) widget.
2//!
3//! This module contains the geometry primitives ([`RawPoint`], [`RawLine`]),
4//! the map content types ([`MapPoint`], [`MapSegment`], [`MapLabel`]) and the
5//! customization points of the widget: [`MapSettings`],
6//! [`Style`](super::theme::Style), [`VisibilitySetting`],
7//! [`ContextMenuManager`] and [`NodeTemplate`]. The color palette a `Style`
8//! paints with lives in [`super::theme`], via [`MapTheme`](super::theme::MapTheme).
9
10use crate::map::theme::{ColorMode, Style, Theme};
11use egui::{Align2, Color32, FontFamily, FontId, Painter, Pos2, Ui};
12use rstar::AABB;
13use std::convert::{From, Into};
14use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub};
15use std::time::Instant;
16
17/// A point (or vector) in 2D map coordinates.
18///
19/// `RawPoint` supports component-wise arithmetic: [`Mul`], [`Div`],
20/// [`MulAssign`] and [`DivAssign`] with `f32` and the common integer types, and
21/// [`Add`]/[`Sub`] with other points (by value or by reference). It also
22/// converts from and to `[f32; 2]`, integer arrays and [`egui::Pos2`].
23///
24/// # Examples
25///
26/// ```
27/// use egui_map::map::objects::RawPoint;
28///
29/// let a = RawPoint::new(1.0, 2.0);
30/// let b = RawPoint::new(3.0, -1.0);
31///
32/// assert_eq!((a + b).components, [4.0, 1.0]);
33/// assert_eq!((a * 2.0f32).components, [2.0, 4.0]);
34/// ```
35#[derive(Copy, Clone, Debug, PartialEq)]
36pub struct RawPoint {
37    /// The `x` and `y` components of the point.
38    pub components: [f32; 2],
39}
40
41impl RawPoint {
42    /// Creates a point from its `x` and `y` coordinates.
43    pub fn new(x: f32, y: f32) -> Self {
44        Self { components: [x, y] }
45    }
46}
47
48impl rstar::Point for RawPoint {
49    type Scalar = f32;
50    const DIMENSIONS: usize = 2;
51
52    fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self {
53        let mut components = [0.0; 2];
54        for (i, component) in components.iter_mut().enumerate() {
55            *component = generator(i);
56        }
57        Self { components }
58    }
59
60    fn nth(&self, index: usize) -> Self::Scalar {
61        self.components[index]
62    }
63
64    fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar {
65        &mut self.components[index]
66    }
67}
68
69impl Default for RawPoint {
70    fn default() -> Self {
71        Self::new(0.00, 0.00)
72    }
73}
74
75impl Mul<i64> for RawPoint {
76    type Output = Self;
77
78    fn mul(self, rhs: i64) -> Self::Output {
79        Self {
80            components: [
81                self.components[0] * rhs as f32,
82                self.components[1] * rhs as f32,
83            ],
84        }
85    }
86}
87
88impl Mul<i32> for RawPoint {
89    type Output = Self;
90
91    fn mul(self, rhs: i32) -> Self::Output {
92        Self {
93            components: [
94                self.components[0] * rhs as f32,
95                self.components[1] * rhs as f32,
96            ],
97        }
98    }
99}
100
101impl Mul<u64> for RawPoint {
102    type Output = Self;
103
104    fn mul(self, rhs: u64) -> Self::Output {
105        Self {
106            components: [
107                self.components[0] * rhs as f32,
108                self.components[1] * rhs as f32,
109            ],
110        }
111    }
112}
113
114impl Mul<u32> for RawPoint {
115    type Output = Self;
116
117    fn mul(self, rhs: u32) -> Self::Output {
118        Self {
119            components: [
120                self.components[0] * rhs as f32,
121                self.components[1] * rhs as f32,
122            ],
123        }
124    }
125}
126
127impl Mul<f32> for RawPoint {
128    type Output = Self;
129
130    fn mul(self, rhs: f32) -> Self::Output {
131        Self {
132            components: [self.components[0] * rhs, self.components[1] * rhs],
133        }
134    }
135}
136
137impl MulAssign<i64> for RawPoint {
138    fn mul_assign(&mut self, rhs: i64) {
139        self.components[0] = self.components[0] * rhs as f32;
140        self.components[1] = self.components[1] * rhs as f32;
141    }
142}
143
144impl MulAssign<i32> for RawPoint {
145    fn mul_assign(&mut self, rhs: i32) {
146        self.components[0] = self.components[0] * rhs as f32;
147        self.components[1] = self.components[1] * rhs as f32;
148    }
149}
150
151impl MulAssign<u64> for RawPoint {
152    fn mul_assign(&mut self, rhs: u64) {
153        self.components[0] = self.components[0] * rhs as f32;
154        self.components[1] = self.components[1] * rhs as f32;
155    }
156}
157
158impl MulAssign<u32> for RawPoint {
159    fn mul_assign(&mut self, rhs: u32) {
160        self.components[0] = self.components[0] * rhs as f32;
161        self.components[1] = self.components[1] * rhs as f32;
162    }
163}
164
165impl MulAssign<f32> for RawPoint {
166    fn mul_assign(&mut self, rhs: f32) {
167        self.components[0] = self.components[0] * rhs;
168        self.components[1] = self.components[1] * rhs;
169    }
170}
171
172impl Div<i64> for RawPoint {
173    type Output = Self;
174
175    fn div(self, rhs: i64) -> Self::Output {
176        Self {
177            components: [
178                self.components[0] / rhs as f32,
179                self.components[1] / rhs as f32,
180            ],
181        }
182    }
183}
184
185impl Div<i32> for RawPoint {
186    type Output = Self;
187
188    fn div(self, rhs: i32) -> Self::Output {
189        Self {
190            components: [
191                self.components[0] / rhs as f32,
192                self.components[1] / rhs as f32,
193            ],
194        }
195    }
196}
197
198impl Div<u64> for RawPoint {
199    type Output = Self;
200
201    fn div(self, rhs: u64) -> Self::Output {
202        Self {
203            components: [
204                self.components[0] / rhs as f32,
205                self.components[1] / rhs as f32,
206            ],
207        }
208    }
209}
210
211impl Div<u32> for RawPoint {
212    type Output = Self;
213
214    fn div(self, rhs: u32) -> Self::Output {
215        Self {
216            components: [
217                self.components[0] / rhs as f32,
218                self.components[1] / rhs as f32,
219            ],
220        }
221    }
222}
223
224impl Div<f32> for RawPoint {
225    type Output = Self;
226
227    fn div(self, rhs: f32) -> Self::Output {
228        Self {
229            components: [self.components[0] / rhs, self.components[1] / rhs],
230        }
231    }
232}
233
234impl DivAssign<i64> for RawPoint {
235    fn div_assign(&mut self, rhs: i64) {
236        self.components[0] = self.components[0] / rhs as f32;
237        self.components[1] = self.components[1] / rhs as f32;
238    }
239}
240
241impl DivAssign<i32> for RawPoint {
242    fn div_assign(&mut self, rhs: i32) {
243        self.components[0] = self.components[0] / rhs as f32;
244        self.components[1] = self.components[1] / rhs as f32;
245    }
246}
247
248impl DivAssign<u64> for RawPoint {
249    fn div_assign(&mut self, rhs: u64) {
250        self.components[0] = self.components[0] / rhs as f32;
251        self.components[1] = self.components[1] / rhs as f32;
252    }
253}
254
255impl DivAssign<u32> for RawPoint {
256    fn div_assign(&mut self, rhs: u32) {
257        self.components[0] = self.components[0] / rhs as f32;
258        self.components[1] = self.components[1] / rhs as f32;
259    }
260}
261
262impl DivAssign<f32> for RawPoint {
263    fn div_assign(&mut self, rhs: f32) {
264        self.components[0] = self.components[0] / rhs;
265        self.components[1] = self.components[1] / rhs;
266    }
267}
268
269impl Add<RawPoint> for RawPoint {
270    type Output = RawPoint;
271    fn add(self, rhs: RawPoint) -> Self::Output {
272        Self {
273            components: [
274                self.components[0] + rhs.components[0],
275                self.components[1] + rhs.components[1],
276            ],
277        }
278    }
279}
280
281impl Sub<RawPoint> for RawPoint {
282    type Output = RawPoint;
283    fn sub(self, rhs: RawPoint) -> Self::Output {
284        Self {
285            components: [
286                self.components[0] - rhs.components[0],
287                self.components[1] - rhs.components[1],
288            ],
289        }
290    }
291}
292
293impl Add<&RawPoint> for RawPoint {
294    type Output = RawPoint;
295    fn add(self, rhs: &RawPoint) -> Self::Output {
296        Self {
297            components: [
298                self.components[0] + rhs.components[0],
299                self.components[1] + rhs.components[1],
300            ],
301        }
302    }
303}
304
305impl Sub<&RawPoint> for RawPoint {
306    type Output = RawPoint;
307    fn sub(self, rhs: &RawPoint) -> Self::Output {
308        Self {
309            components: [
310                self.components[0] - rhs.components[0],
311                self.components[1] - rhs.components[1],
312            ],
313        }
314    }
315}
316
317impl From<[f32; 2]> for RawPoint {
318    fn from(value: [f32; 2]) -> Self {
319        Self { components: value }
320    }
321}
322
323impl From<Pos2> for RawPoint {
324    fn from(value: Pos2) -> Self {
325        Self {
326            components: [value.x, value.y],
327        }
328    }
329}
330
331impl From<[i64; 2]> for RawPoint {
332    fn from(value: [i64; 2]) -> Self {
333        Self {
334            components: [value[0] as f32, value[1] as f32],
335        }
336    }
337}
338
339impl From<[i32; 2]> for RawPoint {
340    fn from(value: [i32; 2]) -> Self {
341        Self {
342            components: [value[0] as f32, value[1] as f32],
343        }
344    }
345}
346
347impl From<[i16; 2]> for RawPoint {
348    fn from(value: [i16; 2]) -> Self {
349        Self {
350            components: [value[0] as f32, value[1] as f32],
351        }
352    }
353}
354
355impl From<[i8; 2]> for RawPoint {
356    fn from(value: [i8; 2]) -> Self {
357        Self {
358            components: [value[0] as f32, value[1] as f32],
359        }
360    }
361}
362
363impl From<RawPoint> for [f32; 2] {
364    fn from(val: RawPoint) -> Self {
365        [val.components[0], val.components[1]]
366    }
367}
368
369impl From<RawPoint> for Pos2 {
370    fn from(val: RawPoint) -> Self {
371        Pos2::from(val.components)
372    }
373}
374
375/// A straight line segment between two [`RawPoint`]s.
376#[derive(Copy, Clone, Debug)]
377pub struct RawLine {
378    /// The two end points of the segment.
379    pub points: [RawPoint; 2],
380}
381
382impl RawLine {
383    /// Creates a segment between `a` and `b`.
384    pub fn new(a: RawPoint, b: RawPoint) -> Self {
385        Self { points: [a, b] }
386    }
387
388    /// Returns the Euclidean distance between the two end points.
389    pub fn distance(self) -> f32 {
390        let x = self.points[0].components[0] - self.points[1].components[0];
391        let y = self.points[0].components[1] - self.points[1].components[1];
392        (x.powi(2) + y.powi(2)).sqrt()
393    }
394
395    /// Returns the point halfway between the two end points.
396    pub fn midpoint(self) -> RawPoint {
397        let x = (self.points[0].components[0] + self.points[1].components[0]) / 2.0;
398        let y = (self.points[0].components[1] + self.points[1].components[1]) / 2.0;
399        RawPoint::new(x, y)
400    }
401
402    /// Returns the Euclidean distance from `point` to the closest point on
403    /// this segment.
404    ///
405    /// The closest point is the perpendicular projection of `point` onto the
406    /// segment's supporting line, clamped to the segment itself; for a
407    /// zero-length segment it is simply the distance to the endpoint.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// use egui_map::map::objects::{RawLine, RawPoint};
413    ///
414    /// let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
415    /// assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
416    /// // Beyond the end of the segment, the endpoint is the closest point.
417    /// assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
418    /// ```
419    pub fn distance_to_point(self, point: RawPoint) -> f32 {
420        let [a, b] = self.points;
421        let ab = b - a;
422        let ap = point - a;
423        let len_sq = ab.components[0].powi(2) + ab.components[1].powi(2);
424        if len_sq == 0.0 {
425            // Degenerate segment: both endpoints coincide.
426            return (ap.components[0].powi(2) + ap.components[1].powi(2)).sqrt();
427        }
428        let t = ((ap.components[0] * ab.components[0] + ap.components[1] * ab.components[1])
429            / len_sq)
430            .clamp(0.0, 1.0);
431        let closest = a + ab * t;
432        let d = point - closest;
433        (d.components[0].powi(2) + d.components[1].powi(2)).sqrt()
434    }
435}
436
437impl From<RawLine> for [Pos2; 2] {
438    fn from(val: RawLine) -> Self {
439        let position1 = val.points[0].into();
440        let position2 = val.points[1].into();
441        [position1, position2]
442    }
443}
444
445impl From<[[i64; 2]; 2]> for RawLine {
446    fn from(value: [[i64; 2]; 2]) -> Self {
447        Self {
448            points: [RawPoint::from(value[0]), RawPoint::from(value[1])],
449        }
450    }
451}
452
453/// A free-floating text label drawn on the map.
454///
455/// Labels are installed with [`Map::add_labels`](super::Map::add_labels).
456#[derive(Clone, Debug)]
457pub struct MapLabel {
458    /// The text to display.
459    pub text: String,
460    /// The position of the label's center.
461    pub center: Pos2,
462}
463
464impl Default for MapLabel {
465    fn default() -> Self {
466        MapLabel::new()
467    }
468}
469
470impl MapLabel {
471    /// Creates an empty label centered at the origin.
472    pub fn new() -> Self {
473        MapLabel {
474            text: String::new(),
475            center: Pos2::new(0.00, 0.00),
476        }
477    }
478}
479
480/// A connection line between two points on the map, ready to be stored in an
481/// [`rstar::RTree`].
482///
483/// Mirrors `sde::objects::SdeSegment`'s shape (`id`, `point1`, `point2`) so
484/// callers that already hold `sde` connection data can build one with a
485/// straight field-for-field copy; the only structural difference is `f32`
486/// instead of `f64` for the coordinates, matching `egui`'s own coordinate
487/// type (`egui` — and therefore this widget — doesn't work in `f64`).
488///
489/// Lines are installed with [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines),
490/// keyed by an id that nodes reference through [`MapPoint::connections`].
491/// The bounding box the R-tree uses for broad-phase viewport culling and
492/// hit-testing is computed on demand from `point1`/`point2` in
493/// [`envelope`](rstar::RTreeObject::envelope) rather than cached on the
494/// struct, same as `SdeSegment`.
495#[derive(Clone, Copy, Debug, PartialEq)]
496pub struct MapSegment {
497    /// Identifier shared with the line key (and with the
498    /// [`MapPoint::connections`] of the endpoint nodes).
499    pub id: (usize, usize),
500    /// One endpoint of the segment, in map coordinates.
501    pub point1: [f32; 2],
502    /// The other endpoint of the segment, in map coordinates.
503    pub point2: [f32; 2],
504}
505
506impl MapSegment {
507    /// Creates a segment for `id` between `point1` and `point2`.
508    pub fn new(id: (usize, usize), point1: [f32; 2], point2: [f32; 2]) -> Self {
509        Self { id, point1, point2 }
510    }
511
512    /// The segment geometry as a [`RawLine`], for the distance/midpoint math
513    /// callers already get from that type.
514    pub(crate) fn raw_line(&self) -> RawLine {
515        RawLine::new(RawPoint::from(self.point1), RawPoint::from(self.point2))
516    }
517}
518
519impl rstar::RTreeObject for MapSegment {
520    type Envelope = AABB<[f32; 2]>;
521
522    fn envelope(&self) -> Self::Envelope {
523        AABB::from_corners(
524            [
525                self.point1[0].min(self.point2[0]),
526                self.point1[1].min(self.point2[1]),
527            ],
528            [
529                self.point1[0].max(self.point2[0]),
530                self.point1[1].max(self.point2[1]),
531            ],
532        )
533    }
534}
535
536/// A node on the map: an id, a 2D position and an optional display name.
537///
538/// Mirrors `sde::objects::SdePoint`'s shape (public `coords`/`id`/`name`/
539/// `connections` fields) so callers that already hold `sde` map-query data
540/// can build one with a straight field-for-field copy. Structural
541/// differences from `SdePoint`:
542///
543/// - `f32` instead of `f64` for `coords`, matching `egui`'s own coordinate
544///   type.
545/// - 2 components instead of 3: this widget only ever renders a 2D map, so
546///   there's no third axis to carry.
547/// - `id` is a plain `usize`, not `Option<usize>`. `SdePoint` uses `None`
548///   for a bare coordinate with no entity behind it (e.g. a bounding-box
549///   corner); every `MapPoint` loaded into the widget represents a real,
550///   placed node whose id is used directly as the point-set `HashMap` key
551///   and the kd-tree payload (see [`Map::add_hashmap_points`]), so an
552///   optional id would just push an `.unwrap()` (or a silently dropped
553///   node) into those call sites with no caller ever passing `None`.
554///
555/// Nodes are loaded into the widget through
556/// [`Map::add_hashmap_points`](super::Map::add_hashmap_points), keyed by their
557/// id.
558#[derive(Clone, Debug, PartialEq)]
559pub struct MapPoint {
560    /// Position of the node, in map coordinates.
561    pub coords: [f32; 2],
562    /// Node identifier, used for lookups, notifications and markers.
563    pub id: usize,
564    /// Display name shown next to the node; `None` if it was never set.
565    pub name: Option<String>,
566    /// Ids of the lines connecting this node with others.
567    ///
568    /// Each entry must match a key of the map passed to
569    /// [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines) (and
570    /// [`MapSegment::id`]). The usual pattern is to push the same pair into
571    /// the `connections` of **both** endpoint nodes. Line visibility is
572    /// computed from the segment bounding boxes (R-tree), not from node
573    /// visibility, so a line is drawn whenever its bounding box intersects
574    /// the viewport.
575    pub connections: Vec<(usize, usize)>,
576    /// Persistent fill color for this node's default circle, in place of
577    /// [`NodeStyle::fill_color`](super::NodeStyle::fill_color). `None`
578    /// (the default) keeps today's behavior of every node sharing the
579    /// same style color.
580    ///
581    /// Only consulted by the built-in circle drawn when no
582    /// [`NodeTemplate`] is installed -- a custom template receives this
583    /// same `MapPoint` and decides for itself whether/how to use `color`.
584    pub color: Option<Color32>,
585}
586
587impl MapPoint {
588    /// Creates a node with the given `id` at the given map coordinates.
589    pub fn new(id: usize, coords: [f32; 2]) -> MapPoint {
590        MapPoint {
591            coords,
592            id,
593            connections: Vec::new(),
594            name: None,
595            color: None,
596        }
597    }
598
599    /// Returns the node identifier.
600    pub fn get_id(&self) -> usize {
601        self.id
602    }
603
604    /// Returns the node display name (empty if it was never set).
605    pub fn get_name(&self) -> String {
606        self.name.clone().unwrap_or_default()
607    }
608
609    /// Sets the node display name.
610    pub fn set_name(&mut self, value: String) {
611        self.name = Some(value);
612    }
613}
614
615impl From<std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>> for MapPoint {
616    fn from(value: std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>) -> Self {
617        let k = value.get();
618        k.clone()
619    }
620}
621
622#[derive(Clone)]
623pub(crate) struct MapBounds {
624    pub min: RawPoint,
625    pub max: RawPoint,
626    pub pos: RawPoint,
627    pub dist: f32,
628}
629
630impl MapBounds {
631    pub fn new() -> Self {
632        MapBounds {
633            min: RawPoint::default(),
634            max: RawPoint::default(),
635            pos: RawPoint::default(),
636            dist: 0.0,
637        }
638    }
639}
640
641impl Default for MapBounds {
642    fn default() -> Self {
643        MapBounds::new()
644    }
645}
646
647pub(crate) struct TextSettings {
648    pub position: RawPoint,
649    pub anchor: Align2,
650    pub text: String,
651    pub size: f32,
652    pub family: FontFamily,
653    pub text_color: Color32,
654}
655
656/// Configuration of a [`Map`](super::Map) widget.
657///
658/// [`MapSettings::default()`] provides sensible zoom limits plus a light and a
659/// dark theme; the widget picks the style to apply based on
660/// [`egui::Visuals::dark_mode`], using `styles[0]` in light mode and
661/// `styles[1]` in dark mode.
662#[derive(Clone, Debug)]
663pub struct MapSettings {
664    /// Maximum zoom factor.
665    pub max_zoom: f32,
666    /// Minimum zoom factor.
667    pub min_zoom: f32,
668    /// Zoom threshold above which connection lines become visible.
669    pub line_visible_zoom: f32,
670    /// Zoom threshold above which node names become visible when
671    /// [`node_text_visibility`](Self::node_text_visibility) is
672    /// [`VisibilitySetting::Always`].
673    pub label_visible_zoom: f32,
674    /// Controls when node names are displayed.
675    pub node_text_visibility: VisibilitySetting,
676    /// Effect drawn on nodes registered with
677    /// [`Map::update_marker`](super::Map::update_marker).
678    ///
679    /// Persistent, so it keeps the app repainting for as long as a marker
680    /// exists. Ignored when a [`NodeTemplate`] is installed.
681    ///
682    /// Node *state* set through [`NodeHandle`](super::NodeHandle) picks its own
683    /// effect per node and does not read this field.
684    pub marker_animation: SteadyAnimation,
685    /// Font size, **in screen pixels**, of the node names.
686    ///
687    /// This is a screen-space size: it deliberately does *not* scale with the
688    /// zoom factor, so a name stays exactly as readable when the map is zoomed
689    /// all the way out as when it is zoomed in. Because the nodes pack closer
690    /// together as you zoom out while the names keep their size, names take up
691    /// proportionally more of the view down there — use
692    /// [`label_visible_zoom`](Self::label_visible_zoom) or
693    /// [`node_text_visibility`](Self::node_text_visibility) to control when
694    /// they are worth showing at all.
695    pub node_text_size: f32,
696    /// Font size, **in screen pixels**, of the free-floating [`MapLabel`]s.
697    ///
698    /// Screen-space, exactly like [`node_text_size`](Self::node_text_size).
699    pub label_text_size: f32,
700    /// Per-mode styles; index `0` is used in light mode, index `1` in dark
701    /// mode. Their colors are kept in sync with the active
702    /// [`MapTheme`](super::theme::MapTheme) -- see
703    /// [`Map::set_theme`](super::Map::set_theme) -- rather than set here.
704    pub styles: Vec<Style>,
705}
706
707impl MapSettings {
708    /// Creates settings with all zoom thresholds set to `0.0` and a single
709    /// transparent style.
710    ///
711    /// Prefer [`MapSettings::default()`] unless you really need to build the
712    /// configuration from scratch.
713    pub fn new() -> Self {
714        MapSettings {
715            max_zoom: 0.0,
716            min_zoom: 0.0,
717            line_visible_zoom: 0.0,
718            label_visible_zoom: 0.0,
719            node_text_visibility: VisibilitySetting::Always,
720            marker_animation: SteadyAnimation::Blink,
721            node_text_size: 12.0,
722            label_text_size: 24.0,
723            styles: vec![Style::new()],
724        }
725    }
726}
727
728impl Default for MapSettings {
729    /// Returns the default configuration: zoom from `0.1` to `2.0`, connection
730    /// lines visible above `0.2`, node names above `0.58`, and built-in light
731    /// and dark themes.
732    fn default() -> Self {
733        let mut obj = MapSettings {
734            max_zoom: 2.0,
735            min_zoom: 0.1,
736            line_visible_zoom: 0.2,
737            label_visible_zoom: 0.58,
738            node_text_visibility: VisibilitySetting::Always,
739            marker_animation: SteadyAnimation::Blink,
740            node_text_size: 12.0,
741            label_text_size: 24.0,
742            styles: Vec::new(),
743        };
744
745        // The border/background colors below are placeholders, overwritten
746        // by `Map::assign_visual_style` from egui's own visuals on the first
747        // frame. The node/text/alert/line colors instead come from the
748        // default `MapTheme` (see `Map::set_theme`) so they never duplicate
749        // what `Theme::colors` already defines -- `Map::apply_theme_colors`
750        // keeps them in sync with whichever `MapTheme` is installed.
751        let light = Theme::default().colors(ColorMode::Light);
752        let dark = Theme::default().colors(ColorMode::Dark);
753
754        // light Theme
755        obj.styles.push(Style {
756            border: Some(egui::Stroke {
757                width: 2.0,
758                color: Color32::from_rgb(216, 142, 58),
759            }),
760            line: Some(egui::Stroke {
761                width: 2.0,
762                color: light.segment,
763            }),
764            fill_color: light.node,
765            text_color: light.text,
766            font: Some(FontId::new(12.00, FontFamily::Proportional)),
767            background_color: Color32::WHITE,
768            alert_color: light.alert,
769        });
770
771        // Dark Theme
772        obj.styles.push(Style {
773            border: Some(egui::Stroke {
774                width: 2.0,
775                color: Color32::GOLD,
776            }),
777            line: Some(egui::Stroke {
778                width: 2.0,
779                color: dark.segment,
780            }),
781            fill_color: dark.node,
782            text_color: dark.text,
783            font: Some(FontId::new(12.00, FontFamily::Proportional)),
784            background_color: Color32::DARK_GRAY,
785            alert_color: dark.alert,
786        });
787        obj
788    }
789}
790
791/// A built-in effect that plays once and ends.
792///
793/// Anchored to the [`Instant`] an event happened, these are the animations
794/// reached through [`NodeHandle`](super::NodeHandle): `map.node(id)?.ripple(t)`.
795/// The widget drops the notification and stops repainting once the effect
796/// finishes. See [`crate::map::animation`] for what each looks like and how
797/// long it runs.
798///
799/// Ignored when a [`NodeTemplate`] is installed — the template's
800/// `notification_ui` takes over. The effects stay reachable there through
801/// [`Animation`](crate::map::animation::Animation).
802#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
803pub enum NodeAnimation {
804    /// Expanding, fading disc. Reads as "one thing happened here".
805    #[default]
806    Pulse,
807    /// Three staggered expanding rings. Reads as "activity is ongoing".
808    Ripple,
809    /// A ring that empties clockwise. Reads as "how old is this information".
810    CountdownArc,
811    /// A disc that overshoots its size and settles. For nodes that just appeared.
812    ScaleIn,
813    /// Four ticks converging on the node. Reads as "target acquired".
814    Crosshair,
815}
816
817/// A built-in effect that runs until it is cleared.
818///
819/// Named after how long it lasts rather than after who uses it, because it has
820/// two consumers: node state set through [`NodeHandle`](super::NodeHandle)
821/// (`map.node(id)?.halo()`), and markers registered with
822/// [`Map::update_marker`](super::Map::update_marker), which pick their look
823/// with [`MapSettings::marker_animation`].
824///
825/// These never end, so the widget keeps requesting repaints for as long as one
826/// is active. That is fine for the handful of elements they are meant for, but
827/// it does keep the app redrawing — see [`crate::map::animation`].
828#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
829pub enum SteadyAnimation {
830    /// Thick ring blinking on and off. The long-standing marker look.
831    #[default]
832    Blink,
833    /// Ring whose opacity breathes in and out. Calmer than [`Self::Blink`].
834    Halo,
835    /// A dot circling the node. Reads as "under observation".
836    Orbit,
837}
838
839/// Which endpoint a [`SegmentAnimation::Comet`] pass starts from.
840///
841/// A segment's own endpoint order (`a`, `b` as loaded through
842/// [`Map::add_lines`](super::Map::add_lines)) is not usually meaningful to a
843/// caller — naming the two ends [`Self::Forward`]/[`Self::Reverse`] instead
844/// keeps the choice about the animation's direction, not about internal
845/// storage order.
846#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
847pub enum CometDirection {
848    /// From the segment's first endpoint to its second.
849    #[default]
850    Forward,
851    /// From the segment's second endpoint to its first.
852    Reverse,
853}
854
855/// A built-in effect that plays once and ends, for a segment.
856///
857/// Anchored to the [`Instant`] an event happened, these are the animations
858/// reached through [`SegmentHandle`](super::SegmentHandle):
859/// `map.segment(id)?.flash(t)`. The widget drops the notification and stops
860/// repainting once the effect finishes. See [`crate::map::animation`] for
861/// what each looks like and how long it runs.
862///
863/// Ignored when a [`SegmentTemplate`] is installed — the template's
864/// `segment_notification_ui` takes over. The effect stays reachable there
865/// through [`Animation`](crate::map::animation::Animation).
866#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
867pub enum SegmentAnimation {
868    /// A brief bright flash on the line that fades back out. The segment
869    /// analogue of [`NodeAnimation::Pulse`] — reads as "something happened on
870    /// this route".
871    #[default]
872    FlashDecay,
873    /// A single dot pass from one endpoint to the other, then gone — the
874    /// event-driven counterpart to [`SteadySegmentAnimation::Comet`]. Reads
875    /// as "one thing moved along this route just now", direction included,
876    /// rather than "traffic keeps flowing this way".
877    Comet(CometDirection),
878    /// The line drawing itself in from the first endpoint to the second,
879    /// then gone. Reads as "this route was just established" rather than
880    /// "something travelled along it".
881    Wipe,
882}
883
884/// A built-in effect that runs until it is cleared, for a segment.
885///
886/// Reached through node state set on [`SegmentHandle`](super::SegmentHandle)
887/// (`map.segment(id)?.comet()` / `.dash()`). Like [`SteadyAnimation`], these
888/// never end, so the widget keeps requesting repaints for as long as one is
889/// active — fine for a handful of highlighted routes, not for every segment
890/// on the map.
891#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
892pub enum SteadySegmentAnimation {
893    /// A dot travelling from one endpoint to the other and looping. Reads as
894    /// "this is the direction of flow".
895    #[default]
896    Comet,
897    /// A dashed line whose pattern slides along the segment ("marching
898    /// ants"). Reads as "route", classic for a path someone might follow.
899    Dash,
900    /// A localized band of brightness travelling the length of the segment
901    /// and looping, fading out before it reaches either end rather than
902    /// snapping back. Reads as "flow", softer and less busy than [`Self::Dash`].
903    GlowBand,
904    /// A row of arrow shapes sliding along the segment, pointing the way.
905    /// Reads as "direction of travel", more explicit than [`Self::Comet`]'s
906    /// single dot.
907    Chevrons,
908}
909
910/// Controls when the name of a node is displayed next to it.
911#[derive(Clone, Debug, PartialEq)]
912pub enum VisibilitySetting {
913    /// Never show node names.
914    Hidden,
915    /// Only show the name of the node closest to the mouse pointer.
916    Hover,
917    /// Always show node names, subject to [`MapSettings::label_visible_zoom`].
918    Always,
919}
920
921/// Provides the contents of the widget's right-click context menu.
922///
923/// Install an implementation with
924/// [`Map::set_context_manager`](super::Map::set_context_manager).
925///
926/// # Examples
927///
928/// ```
929/// use egui_map::map::objects::ContextMenuManager;
930///
931/// struct MyMenu;
932///
933/// impl ContextMenuManager for MyMenu {
934///     fn ui(&self, ui: &mut egui::Ui) {
935///         ui.label("Hello from the map!");
936///     }
937/// }
938/// ```
939pub trait ContextMenuManager {
940    /// Builds the menu contents; called every frame while the menu is open.
941    fn ui(&self, ui: &mut Ui);
942}
943
944/// Customizes how nodes and their visual effects are rendered.
945///
946/// When a template is installed with
947/// [`Map::set_node_template`](super::Map::set_node_template), the widget
948/// delegates all node painting to it instead of using the built-in shapes and
949/// animations — including the node name labels, so draw the name yourself in
950/// [`NodeTemplate::node_ui`] if you need it.
951///
952/// The positions passed to these methods are in screen coordinates: already
953/// scaled by `zoom` and translated to the viewport origin. Multiply every size
954/// by `zoom` so your shapes scale together with the map.
955///
956/// # Animation idioms
957///
958/// egui only repaints on demand, so any method that animates (a blinking
959/// marker, a fading notification, ...) must call
960/// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) to keep the
961/// frames coming. Time-driven effects are usually computed from
962/// [`Instant::now()`] (see `initial_time` in
963/// [`NodeTemplate::notification_ui`]) or from the system clock.
964///
965/// # Examples
966///
967/// A node drawn as a rounded box with its name inside, plus a notification
968/// animation that expands and fades out over two seconds:
969///
970/// ```
971/// use egui_map::map::objects::{MapPoint, NodeTemplate, NotificationContext, MarkerContext};
972/// use egui::{Align2, Color32, CornerRadius, FontId, Pos2, Rect, Stroke, Ui, Vec2};
973/// use std::time::Instant;
974///
975/// struct BoxedNodes;
976///
977/// impl NodeTemplate for BoxedNodes {
978///     fn node_ui(&self, ui: &mut Ui, position: Pos2, zoom: f32, point: &MapPoint) {
979///         // Multiply every size by `zoom` so the node scales with the map.
980///         let rect = Rect::from_center_size(position, Vec2::new(90.0 * zoom, 35.0 * zoom));
981///         let rounding = CornerRadius::same((10.0 * zoom) as u8);
982///         let painter = ui.painter();
983///         painter.rect_filled(rect, rounding, ui.visuals().extreme_bg_color);
984///         painter.rect_stroke(
985///             rect,
986///             rounding,
987///             Stroke::new(4.0 * zoom, Color32::WHITE),
988///             egui::StrokeKind::Middle,
989///         );
990///         painter.text(
991///             position,
992///             Align2::CENTER_CENTER,
993///             point.get_name(),
994///             FontId::proportional(12.0 * zoom),
995///             Color32::WHITE,
996///         );
997///     }
998///
999///     fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool {
1000///         let secs = Instant::now().duration_since(ctx.initial_time).as_secs_f32();
1001///         // Expand the stroke and fade the color out over 2 seconds.
1002///         let alpha = (1.0 - secs / 2.0).clamp(0.0, 1.0);
1003///         let fading = Color32::from_rgba_unmultiplied(
1004///             ctx.color.r(),
1005///             ctx.color.g(),
1006///             ctx.color.b(),
1007///             (255.0 * alpha) as u8,
1008///         );
1009///         let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
1010///         ui.painter().rect_stroke(
1011///             rect,
1012///             CornerRadius::same((10.0 * ctx.zoom) as u8),
1013///             Stroke::new((4.0 + 25.0 * secs) * ctx.zoom, fading),
1014///             egui::StrokeKind::Middle,
1015///         );
1016///         // Keep the animation frames coming.
1017///         ui.ctx().request_repaint();
1018///         // Returning `false` removes the notification.
1019///         secs < 2.0
1020///     }
1021///     # fn selection_ui(&self, ui: &mut Ui, point: Pos2, zoom: f32) {
1022///     #     let rect = Rect::from_center_size(point, Vec2::new(94.0 * zoom, 39.0 * zoom));
1023///     #     ui.painter().rect_stroke(
1024///     #         rect,
1025///     #         CornerRadius::same((10.0 * zoom) as u8),
1026///     #         Stroke::new(3.0 * zoom, Color32::YELLOW),
1027///     #         egui::StrokeKind::Middle,
1028///     #     );
1029///     # }
1030///     # fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext) {
1031///     #     ui.painter().circle_stroke(ctx.position, 6.0 * ctx.zoom, Stroke::new(2.0 * ctx.zoom, Color32::LIGHT_GREEN));
1032///     #     ui.ctx().request_repaint();
1033///     # }
1034/// }
1035/// ```
1036///
1037/// # Note on `NodeAnimation`/`SteadyAnimation` in the examples above
1038///
1039/// The hidden (`#`-prefixed) stub methods above still take `Pos2`/`f32`
1040/// directly rather than a context struct -- only [`NotificationContext`] and
1041/// [`MarkerContext`] exist; `node_ui`/`selection_ui` were not wide enough to
1042/// need one.
1043pub trait NodeTemplate {
1044    /// Draws a node, replacing the default filled circle.
1045    ///
1046    /// Called every frame for each visible node. The widget no longer draws
1047    /// the node name once a template is installed, so render it here (e.g.
1048    /// with [`Painter::text`](egui::Painter::text)) if you need it.
1049    fn node_ui(&self, ui: &mut Ui, _viewport_position: Pos2, _zoom: f32, _point: &MapPoint);
1050
1051    /// Draws the highlight over the node closest to the mouse pointer.
1052    ///
1053    /// The nearest node is only computed while the pointer is over the map and
1054    /// [`MapSettings::node_text_visibility`] is [`VisibilitySetting::Hover`].
1055    fn selection_ui(&self, ui: &mut Ui, _viewport_position: Pos2, _zoom: f32);
1056
1057    /// Draws the notification effect of a node notified at
1058    /// `ctx.initial_time`.
1059    ///
1060    /// Called every frame for each node passed to
1061    /// [`Map::notify`](super::Map::notify) or animated through
1062    /// [`Map::node`](super::Map::node)'s event methods (`pulse`, `ripple`,
1063    /// ...). `ctx.kind` is which of those was requested and `ctx.node_id` is
1064    /// the id of the node it belongs to -- use them to dispatch to the
1065    /// matching built-in [`Animation`](crate::map::animation::Animation)
1066    /// function (or your own effect) instead of reimplementing every
1067    /// animation by hand. See [`NotificationContext`] for the rest of the
1068    /// fields. Should return `true` while the animation is still playing —
1069    /// remember to call
1070    /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) —;
1071    /// once it returns `false` the notification is discarded.
1072    fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool;
1073
1074    /// Draws a marker over the given node.
1075    ///
1076    /// Called every frame for two different things -- see [`MarkerContext`]
1077    /// for what `ctx.kind`/`ctx.node_id` mean in each case. For animated
1078    /// markers (e.g. a blinking light), drive the effect from the system
1079    /// clock and call
1080    /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint).
1081    fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext);
1082}
1083
1084/// The context passed to [`NodeTemplate::notification_ui`].
1085///
1086/// `#[non_exhaustive]` so a future field can be added here without another
1087/// breaking change to [`NodeTemplate`].
1088#[derive(Clone, Copy, Debug)]
1089#[non_exhaustive]
1090pub struct NotificationContext {
1091    /// The node's screen position: already scaled by `zoom` and translated
1092    /// to the viewport origin.
1093    pub position: Pos2,
1094    /// Multiply every size you draw by this so it scales with the map.
1095    pub zoom: f32,
1096    /// When the notification started -- usually fed into a progress
1097    /// computation like `Instant::now().duration_since(initial_time)`.
1098    pub initial_time: Instant,
1099    /// The color requested for this notification (the node's own color, or
1100    /// the current style's `alert_color` if none was set).
1101    pub color: Color32,
1102    /// Which built-in event effect was requested (`pulse`, `ripple`, ...).
1103    /// Match on this to dispatch to the corresponding
1104    /// [`Animation`](crate::map::animation::Animation) function instead of
1105    /// reimplementing the lookup yourself.
1106    pub kind: NodeAnimation,
1107    /// The id of the node this notification belongs to.
1108    pub node_id: usize,
1109}
1110
1111/// The context passed to [`NodeTemplate::marker_ui`].
1112///
1113/// `#[non_exhaustive]`, like [`NotificationContext`], so a future field can
1114/// be added here without another breaking change.
1115#[derive(Clone, Copy, Debug)]
1116#[non_exhaustive]
1117pub struct MarkerContext {
1118    /// The node's screen position: already scaled by `zoom` and translated
1119    /// to the viewport origin.
1120    pub position: Pos2,
1121    /// Multiply every size you draw by this so it scales with the map.
1122    pub zoom: f32,
1123    /// Which persistent effect to draw. For a node's own lasting state (set
1124    /// through [`Map::node`](super::Map::node)'s `halo`/`blink`/`orbit`)
1125    /// this is whichever of those was requested; for a plain marker
1126    /// (registered with [`Map::update_marker`](super::Map::update_marker))
1127    /// it is always [`MapSettings::marker_animation`], since every marker
1128    /// shares that one setting. There is no way from inside this hook to
1129    /// tell the two *cases* apart -- only which `SteadyAnimation` to draw
1130    /// for whichever one it is.
1131    pub kind: SteadyAnimation,
1132    /// The id of the node the state/marker belongs to (for a marker: the id
1133    /// it points at, not the marker's own id).
1134    pub node_id: usize,
1135}
1136
1137/// Customizes how segments and their visual effects are rendered.
1138///
1139/// When a template is installed with
1140/// [`Map::set_segment_template`](super::Map::set_segment_template), the widget
1141/// delegates all segment painting to it instead of using the built-in stroke
1142/// and animations.
1143///
1144/// Unlike [`NodeTemplate`], these methods receive a bare [`&Painter`](Painter)
1145/// rather than `&mut Ui`. Segments are visited in bulk, every frame, after the
1146/// R-tree viewport culling in `paint_map_lines`; going through `Ui` would cost
1147/// a layout pass per segment, on top of what the culling already had to
1148/// discard. Use [`Painter::ctx`] to reach the [`egui::Context`] — for example
1149/// to call `request_repaint()`.
1150///
1151/// The positions passed to these methods are in screen coordinates: already
1152/// scaled by `zoom` and translated to the viewport origin, same as
1153/// [`NodeTemplate`]'s. Multiply every size by `zoom` so your shapes scale
1154/// together with the map.
1155///
1156/// # Examples
1157///
1158/// A segment drawn as a dashed line, plus a notification that briefly
1159/// thickens and brightens it:
1160///
1161/// ```
1162/// use egui_map::map::objects::{MapSegment, SegmentTemplate};
1163/// use egui::{Color32, Painter, Pos2, Stroke};
1164/// use std::time::Instant;
1165///
1166/// struct DashedRoutes;
1167///
1168/// impl SegmentTemplate for DashedRoutes {
1169///     fn segment_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, _segment: &MapSegment) {
1170///         // A crude dash: short strokes along the segment, spaced in screen
1171///         // pixels so they don't stretch as the map zooms.
1172///         let dir = b - a;
1173///         let len = dir.length();
1174///         let step = 10.0 * zoom;
1175///         let mut travelled = 0.0;
1176///         while travelled < len {
1177///             let start = a + dir * (travelled / len);
1178///             let end = a + dir * ((travelled + step * 0.6).min(len) / len);
1179///             painter.line_segment([start, end], Stroke::new(2.0 * zoom, Color32::GRAY));
1180///             travelled += step;
1181///         }
1182///     }
1183///
1184///     fn segment_notification_ui(
1185///         &self,
1186///         painter: &Painter,
1187///         a: Pos2,
1188///         b: Pos2,
1189///         zoom: f32,
1190///         initial_time: Instant,
1191///         color: Color32,
1192///     ) -> bool {
1193///         let secs = Instant::now().duration_since(initial_time).as_secs_f32();
1194///         let alpha = (1.0 - secs).clamp(0.0, 1.0);
1195///         let fading =
1196///             Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), (255.0 * alpha) as u8);
1197///         painter.line_segment([a, b], Stroke::new(5.0 * zoom, fading));
1198///         painter.ctx().request_repaint();
1199///         secs < 1.0
1200///     }
1201///
1202///     fn segment_state_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, time: f32, color: Color32) {
1203///         let t = (time / 1.6).rem_euclid(1.0);
1204///         painter.circle_filled(a + (b - a) * t, 4.0 * zoom, color);
1205///         painter.ctx().request_repaint();
1206///     }
1207/// }
1208/// ```
1209pub trait SegmentTemplate {
1210    /// Draws a segment, replacing the default stroked line.
1211    ///
1212    /// Called every frame for each segment that survives the R-tree viewport
1213    /// culling in `paint_map_lines`.
1214    fn segment_ui(
1215        &self,
1216        painter: &Painter,
1217        pos_a: Pos2,
1218        pos_b: Pos2,
1219        zoom: f32,
1220        segment: &MapSegment,
1221    );
1222
1223    /// Draws the notification effect of a segment notified through
1224    /// [`Map::segment`](super::Map::segment).
1225    ///
1226    /// Called every frame for each segment carrying an event-driven effect
1227    /// (see [`SegmentHandle`](super::SegmentHandle)). Should return `true`
1228    /// while the animation is still playing — remember to call
1229    /// [`Painter::ctx`]`().request_repaint()` — once it returns `false` the
1230    /// notification is discarded.
1231    fn segment_notification_ui(
1232        &self,
1233        painter: &Painter,
1234        pos_a: Pos2,
1235        pos_b: Pos2,
1236        zoom: f32,
1237        initial_time: Instant,
1238        color: Color32,
1239    ) -> bool;
1240
1241    /// Draws the lasting state effect of a segment (e.g. a travelling dot).
1242    ///
1243    /// Called every frame for each segment with lasting state set through
1244    /// [`Map::segment`](super::Map::segment). `time` is the frame time in
1245    /// seconds (`ui.input(|i| i.time)`), so every element animated this frame
1246    /// shares one clock. For animated state, remember to call
1247    /// [`Painter::ctx`]`().request_repaint()`.
1248    fn segment_state_ui(
1249        &self,
1250        painter: &Painter,
1251        pos_a: Pos2,
1252        pos_b: Pos2,
1253        zoom: f32,
1254        time: f32,
1255        color: Color32,
1256    );
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261    use super::*;
1262    use std::collections::HashMap;
1263
1264    // ---------- RawPoint ----------
1265
1266    #[test]
1267    fn raw_point_new() {
1268        let p = RawPoint::new(3.5, -2.0);
1269        assert_eq!(p.components, [3.5, -2.0]);
1270    }
1271
1272    // ---------- MapSegment ----------
1273
1274    #[test]
1275    fn map_segment_new_computes_tight_aabb() {
1276        let seg = MapSegment::new((1, 2), [10.0, -5.0], [-2.0, 7.0]);
1277        assert_eq!(seg.id, (1, 2));
1278        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1279        assert_eq!(envelope.lower(), [-2.0, -5.0]);
1280        assert_eq!(envelope.upper(), [10.0, 7.0]);
1281        assert_eq!(seg.raw_line().points[0].components, [10.0, -5.0]);
1282        assert_eq!(seg.raw_line().points[1].components, [-2.0, 7.0]);
1283    }
1284
1285    #[test]
1286    fn map_segment_envelope_returns_its_aabb() {
1287        let seg = MapSegment::new((1, 2), [0.0, 0.0], [4.0, 2.0]);
1288        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1289        assert_eq!(envelope.lower(), [0.0, 0.0]);
1290        assert_eq!(envelope.upper(), [4.0, 2.0]);
1291    }
1292
1293    #[test]
1294    fn map_segment_degenerate_line_has_point_aabb() {
1295        // A zero-length segment must still produce a valid (empty-area) AABB.
1296        let seg = MapSegment::new((1, 2), [3.0, 3.0], [3.0, 3.0]);
1297        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1298        assert_eq!(envelope.lower(), [3.0, 3.0]);
1299        assert_eq!(envelope.upper(), [3.0, 3.0]);
1300    }
1301
1302    #[test]
1303    fn raw_point_default() {
1304        let p = RawPoint::default();
1305        assert_eq!(p.components, [0.0, 0.0]);
1306    }
1307
1308    #[test]
1309    fn raw_point_mul_i64() {
1310        let p = RawPoint::new(2.0, -3.0) * 3i64;
1311        assert_eq!(p.components, [6.0, -9.0]);
1312    }
1313
1314    #[test]
1315    fn raw_point_mul_i32() {
1316        let p = RawPoint::new(2.0, -3.0) * 3i32;
1317        assert_eq!(p.components, [6.0, -9.0]);
1318    }
1319
1320    #[test]
1321    fn raw_point_mul_u64() {
1322        let p = RawPoint::new(2.0, -3.0) * 3u64;
1323        assert_eq!(p.components, [6.0, -9.0]);
1324    }
1325
1326    #[test]
1327    fn raw_point_mul_u32() {
1328        let p = RawPoint::new(2.0, -3.0) * 3u32;
1329        assert_eq!(p.components, [6.0, -9.0]);
1330    }
1331
1332    #[test]
1333    fn raw_point_mul_f32() {
1334        let p = RawPoint::new(2.0, -3.0) * 0.5f32;
1335        assert_eq!(p.components, [1.0, -1.5]);
1336    }
1337
1338    #[test]
1339    fn raw_point_mul_assign_i64() {
1340        let mut p = RawPoint::new(2.0, -3.0);
1341        p *= 3i64;
1342        assert_eq!(p.components, [6.0, -9.0]);
1343    }
1344
1345    #[test]
1346    fn raw_point_mul_assign_i32() {
1347        let mut p = RawPoint::new(2.0, -3.0);
1348        p *= 3i32;
1349        assert_eq!(p.components, [6.0, -9.0]);
1350    }
1351
1352    #[test]
1353    fn raw_point_mul_assign_u64() {
1354        let mut p = RawPoint::new(2.0, -3.0);
1355        p *= 3u64;
1356        assert_eq!(p.components, [6.0, -9.0]);
1357    }
1358
1359    #[test]
1360    fn raw_point_mul_assign_u32() {
1361        let mut p = RawPoint::new(2.0, -3.0);
1362        p *= 3u32;
1363        assert_eq!(p.components, [6.0, -9.0]);
1364    }
1365
1366    #[test]
1367    fn raw_point_mul_assign_f32() {
1368        let mut p = RawPoint::new(2.0, -3.0);
1369        p *= 0.5f32;
1370        assert_eq!(p.components, [1.0, -1.5]);
1371    }
1372
1373    #[test]
1374    fn raw_point_div_i64() {
1375        let p = RawPoint::new(6.0, -9.0) / 3i64;
1376        assert_eq!(p.components, [2.0, -3.0]);
1377    }
1378
1379    #[test]
1380    fn raw_point_div_i32() {
1381        let p = RawPoint::new(6.0, -9.0) / 3i32;
1382        assert_eq!(p.components, [2.0, -3.0]);
1383    }
1384
1385    #[test]
1386    fn raw_point_div_u64() {
1387        let p = RawPoint::new(6.0, -9.0) / 3u64;
1388        assert_eq!(p.components, [2.0, -3.0]);
1389    }
1390
1391    #[test]
1392    fn raw_point_div_u32() {
1393        let p = RawPoint::new(6.0, -9.0) / 3u32;
1394        assert_eq!(p.components, [2.0, -3.0]);
1395    }
1396
1397    #[test]
1398    fn raw_point_div_f32() {
1399        let p = RawPoint::new(1.0, -1.5) / 0.5f32;
1400        assert_eq!(p.components, [2.0, -3.0]);
1401    }
1402
1403    #[test]
1404    fn raw_point_div_assign_i64() {
1405        let mut p = RawPoint::new(6.0, -9.0);
1406        p /= 3i64;
1407        assert_eq!(p.components, [2.0, -3.0]);
1408    }
1409
1410    #[test]
1411    fn raw_point_div_assign_i32() {
1412        let mut p = RawPoint::new(6.0, -9.0);
1413        p /= 3i32;
1414        assert_eq!(p.components, [2.0, -3.0]);
1415    }
1416
1417    #[test]
1418    fn raw_point_div_assign_u64() {
1419        let mut p = RawPoint::new(6.0, -9.0);
1420        p /= 3u64;
1421        assert_eq!(p.components, [2.0, -3.0]);
1422    }
1423
1424    #[test]
1425    fn raw_point_div_assign_u32() {
1426        let mut p = RawPoint::new(6.0, -9.0);
1427        p /= 3u32;
1428        assert_eq!(p.components, [2.0, -3.0]);
1429    }
1430
1431    #[test]
1432    fn raw_point_div_assign_f32() {
1433        let mut p = RawPoint::new(1.0, -1.5);
1434        p /= 0.5f32;
1435        assert_eq!(p.components, [2.0, -3.0]);
1436    }
1437
1438    #[test]
1439    fn raw_point_add() {
1440        let a = RawPoint::new(1.0, 2.0);
1441        let b = RawPoint::new(3.0, -4.0);
1442        let c = a + b;
1443        assert_eq!(c.components, [4.0, -2.0]);
1444    }
1445
1446    #[test]
1447    fn raw_point_sub() {
1448        let a = RawPoint::new(1.0, 2.0);
1449        let b = RawPoint::new(3.0, -4.0);
1450        let c = a - b;
1451        assert_eq!(c.components, [-2.0, 6.0]);
1452    }
1453
1454    #[test]
1455    #[allow(clippy::op_ref)] // se prueba a propósito la impl Add<&RawPoint>
1456    fn raw_point_add_ref() {
1457        let a = RawPoint::new(1.0, 2.0);
1458        let b = RawPoint::new(3.0, -4.0);
1459        let c = a + &b;
1460        assert_eq!(c.components, [4.0, -2.0]);
1461        // b sigue siendo usable tras la suma por referencia
1462        assert_eq!(b.components, [3.0, -4.0]);
1463    }
1464
1465    #[test]
1466    #[allow(clippy::op_ref)] // se prueba a propósito la impl Sub<&RawPoint>
1467    fn raw_point_sub_ref() {
1468        let a = RawPoint::new(1.0, 2.0);
1469        let b = RawPoint::new(3.0, -4.0);
1470        let c = a - &b;
1471        assert_eq!(c.components, [-2.0, 6.0]);
1472        assert_eq!(b.components, [3.0, -4.0]);
1473    }
1474
1475    #[test]
1476    fn raw_point_from_f32_array() {
1477        let p = RawPoint::from([1.5f32, -2.5f32]);
1478        assert_eq!(p.components, [1.5, -2.5]);
1479    }
1480
1481    #[test]
1482    fn raw_point_from_i64_array() {
1483        let p = RawPoint::from([3i64, -4i64]);
1484        assert_eq!(p.components, [3.0, -4.0]);
1485    }
1486
1487    #[test]
1488    fn raw_point_from_i32_array() {
1489        let p = RawPoint::from([3i32, -4i32]);
1490        assert_eq!(p.components, [3.0, -4.0]);
1491    }
1492
1493    #[test]
1494    fn raw_point_from_i16_array() {
1495        let p = RawPoint::from([3i16, -4i16]);
1496        assert_eq!(p.components, [3.0, -4.0]);
1497    }
1498
1499    #[test]
1500    fn raw_point_from_i8_array() {
1501        let p = RawPoint::from([3i8, -4i8]);
1502        assert_eq!(p.components, [3.0, -4.0]);
1503    }
1504
1505    #[test]
1506    fn raw_point_from_pos2() {
1507        let p = RawPoint::from(Pos2::new(7.0, 8.0));
1508        assert_eq!(p.components, [7.0, 8.0]);
1509    }
1510
1511    #[test]
1512    fn raw_point_into_f32_array() {
1513        let arr: [f32; 2] = RawPoint::new(7.0, 8.0).into();
1514        assert_eq!(arr, [7.0, 8.0]);
1515    }
1516
1517    #[test]
1518    fn raw_point_into_pos2() {
1519        let pos: Pos2 = RawPoint::new(7.0, 8.0).into();
1520        assert_eq!(pos, Pos2::new(7.0, 8.0));
1521    }
1522
1523    // ---------- RawLine ----------
1524
1525    #[test]
1526    fn raw_line_new() {
1527        let a = RawPoint::new(1.0, 2.0);
1528        let b = RawPoint::new(3.0, 4.0);
1529        let line = RawLine::new(a, b);
1530        assert_eq!(line.points[0].components, [1.0, 2.0]);
1531        assert_eq!(line.points[1].components, [3.0, 4.0]);
1532    }
1533
1534    #[test]
1535    fn raw_line_distance() {
1536        // triángulo 3-4-5
1537        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(3.0, 4.0));
1538        assert_eq!(line.distance(), 5.0);
1539    }
1540
1541    #[test]
1542    fn raw_line_distance_zero() {
1543        let line = RawLine::new(RawPoint::new(2.0, 2.0), RawPoint::new(2.0, 2.0));
1544        assert_eq!(line.distance(), 0.0);
1545    }
1546
1547    #[test]
1548    fn raw_line_midpoint() {
1549        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(4.0, 6.0));
1550        let mid = line.midpoint();
1551        assert_eq!(mid.components, [2.0, 3.0]);
1552    }
1553
1554    #[test]
1555    fn raw_line_distance_to_point_on_segment() {
1556        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1557        assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
1558        assert_eq!(line.distance_to_point(RawPoint::new(5.0, 0.0)), 0.0);
1559    }
1560
1561    #[test]
1562    fn raw_line_distance_to_point_beyond_endpoints() {
1563        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1564        // Past the end of the segment, the closest point is the endpoint.
1565        assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
1566        assert_eq!(line.distance_to_point(RawPoint::new(-3.0, -4.0)), 5.0);
1567    }
1568
1569    #[test]
1570    fn raw_line_distance_to_point_degenerate_segment() {
1571        let line = RawLine::new(RawPoint::new(1.0, 1.0), RawPoint::new(1.0, 1.0));
1572        assert_eq!(line.distance_to_point(RawPoint::new(4.0, 5.0)), 5.0);
1573    }
1574
1575    #[test]
1576    fn raw_line_into_pos2_array() {
1577        let line = RawLine::new(RawPoint::new(1.0, 2.0), RawPoint::new(3.0, 4.0));
1578        let arr: [Pos2; 2] = line.into();
1579        assert_eq!(arr, [Pos2::new(1.0, 2.0), Pos2::new(3.0, 4.0)]);
1580    }
1581
1582    #[test]
1583    fn raw_line_from_i64_arrays() {
1584        let line = RawLine::from([[1i64, 2i64], [3i64, 4i64]]);
1585        assert_eq!(line.points[0].components, [1.0, 2.0]);
1586        assert_eq!(line.points[1].components, [3.0, 4.0]);
1587    }
1588
1589    // ---------- MapStyle ----------
1590
1591    fn full_style() -> Style {
1592        Style {
1593            border: Some(egui::Stroke::new(2.0, Color32::RED)),
1594            line: Some(egui::Stroke::new(4.0, Color32::BLUE)),
1595            fill_color: Color32::GREEN,
1596            text_color: Color32::WHITE,
1597            font: Some(FontId::new(10.0, FontFamily::Proportional)),
1598            background_color: Color32::BLACK,
1599            alert_color: Color32::YELLOW,
1600        }
1601    }
1602
1603    #[test]
1604    fn map_style_new() {
1605        let s = Style::new();
1606        assert!(s.border.is_none());
1607        assert!(s.line.is_none());
1608        assert!(s.font.is_none());
1609        assert_eq!(s.fill_color, Color32::TRANSPARENT);
1610        assert_eq!(s.text_color, Color32::TRANSPARENT);
1611        assert_eq!(s.background_color, Color32::TRANSPARENT);
1612        assert_eq!(s.alert_color, Color32::TRANSPARENT);
1613    }
1614
1615    #[test]
1616    fn map_style_default_equals_new() {
1617        let s = Style::default();
1618        assert!(s.border.is_none());
1619        assert!(s.line.is_none());
1620        assert!(s.font.is_none());
1621    }
1622
1623    #[test]
1624    fn map_style_mul_i64() {
1625        let s = full_style() * 2i64;
1626        assert_eq!(s.border.unwrap().width, 4.0);
1627        assert_eq!(s.line.unwrap().width, 8.0);
1628        assert_eq!(s.font.unwrap().size, 20.0);
1629    }
1630
1631    #[test]
1632    fn map_style_mul_i32() {
1633        let s = full_style() * 2i32;
1634        assert_eq!(s.border.unwrap().width, 4.0);
1635        assert_eq!(s.line.unwrap().width, 8.0);
1636        assert_eq!(s.font.unwrap().size, 20.0);
1637    }
1638
1639    #[test]
1640    fn map_style_mul_f32() {
1641        let s = full_style() * 0.5f32;
1642        assert_eq!(s.border.unwrap().width, 1.0);
1643        assert_eq!(s.line.unwrap().width, 2.0);
1644        assert_eq!(s.font.unwrap().size, 5.0);
1645    }
1646
1647    #[test]
1648    fn map_style_mul_f64() {
1649        let s = full_style() * 0.5f64;
1650        assert_eq!(s.border.unwrap().width, 1.0);
1651        assert_eq!(s.line.unwrap().width, 2.0);
1652        assert_eq!(s.font.unwrap().size, 5.0);
1653    }
1654
1655    #[test]
1656    fn map_style_div_i64() {
1657        let s = full_style() / 2i64;
1658        assert_eq!(s.border.unwrap().width, 1.0);
1659        assert_eq!(s.line.unwrap().width, 2.0);
1660        assert_eq!(s.font.unwrap().size, 5.0);
1661    }
1662
1663    #[test]
1664    fn map_style_div_i32() {
1665        let s = full_style() / 2i32;
1666        assert_eq!(s.border.unwrap().width, 1.0);
1667        assert_eq!(s.line.unwrap().width, 2.0);
1668        assert_eq!(s.font.unwrap().size, 5.0);
1669    }
1670
1671    #[test]
1672    fn map_style_div_f32() {
1673        let s = full_style() / 0.5f32;
1674        assert_eq!(s.border.unwrap().width, 4.0);
1675        assert_eq!(s.line.unwrap().width, 8.0);
1676        assert_eq!(s.font.unwrap().size, 20.0);
1677    }
1678
1679    #[test]
1680    fn map_style_div_f64() {
1681        let s = full_style() / 0.5f64;
1682        assert_eq!(s.border.unwrap().width, 4.0);
1683        assert_eq!(s.line.unwrap().width, 8.0);
1684        assert_eq!(s.font.unwrap().size, 20.0);
1685    }
1686
1687    // ---------- MapLabel ----------
1688
1689    #[test]
1690    fn map_label_new() {
1691        let l = MapLabel::new();
1692        assert_eq!(l.text, String::new());
1693        assert_eq!(l.center, Pos2::new(0.0, 0.0));
1694    }
1695
1696    #[test]
1697    fn map_label_default_equals_new() {
1698        let l = MapLabel::default();
1699        assert_eq!(l.text, String::new());
1700        assert_eq!(l.center, Pos2::new(0.0, 0.0));
1701    }
1702
1703    // ---------- MapPoint ----------
1704
1705    #[test]
1706    fn map_point_new() {
1707        let p = MapPoint::new(42, [1.0, 2.0]);
1708        assert_eq!(p.get_id(), 42);
1709        assert_eq!(p.coords, [1.0, 2.0]);
1710        assert!(p.connections.is_empty());
1711        assert_eq!(p.name, None);
1712        assert_eq!(p.get_name(), String::new());
1713    }
1714
1715    #[test]
1716    fn map_point_set_and_get_name() {
1717        let mut p = MapPoint::new(1, [0.0, 0.0]);
1718        p.set_name("Jita".to_string());
1719        assert_eq!(p.name, Some("Jita".to_string()));
1720        assert_eq!(p.get_name(), "Jita");
1721    }
1722
1723    #[test]
1724    fn map_point_from_occupied_entry() {
1725        let mut map: HashMap<usize, MapPoint> = HashMap::new();
1726        let mut original = MapPoint::new(7, [5.0, 6.0]);
1727        original.set_name("Amarr".to_string());
1728        map.insert(7, original);
1729
1730        use std::collections::hash_map::Entry;
1731        if let Entry::Occupied(entry) = map.entry(7) {
1732            let cloned = MapPoint::from(entry);
1733            assert_eq!(cloned.get_id(), 7);
1734            assert_eq!(cloned.get_name(), "Amarr");
1735            assert_eq!(cloned.coords, [5.0, 6.0]);
1736        } else {
1737            panic!("se esperaba una entrada ocupada");
1738        }
1739    }
1740
1741    // ---------- MapBounds ----------
1742
1743    #[test]
1744    fn map_bounds_new() {
1745        let b = MapBounds::new();
1746        assert_eq!(b.min.components, [0.0, 0.0]);
1747        assert_eq!(b.max.components, [0.0, 0.0]);
1748        assert_eq!(b.pos.components, [0.0, 0.0]);
1749        assert_eq!(b.dist, 0.0);
1750    }
1751
1752    #[test]
1753    fn map_bounds_default_equals_new() {
1754        let b = MapBounds::default();
1755        assert_eq!(b.dist, 0.0);
1756        assert_eq!(b.pos.components, [0.0, 0.0]);
1757    }
1758
1759    // ---------- MapSettings ----------
1760
1761    #[test]
1762    fn map_settings_new() {
1763        let s = MapSettings::new();
1764        assert_eq!(s.max_zoom, 0.0);
1765        assert_eq!(s.min_zoom, 0.0);
1766        assert_eq!(s.line_visible_zoom, 0.0);
1767        assert_eq!(s.label_visible_zoom, 0.0);
1768        assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1769        assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1770        assert_eq!(s.node_text_size, 12.0);
1771        assert_eq!(s.label_text_size, 24.0);
1772        assert_eq!(s.styles.len(), 1);
1773    }
1774
1775    #[test]
1776    fn map_settings_default() {
1777        let s = MapSettings::default();
1778        assert_eq!(s.max_zoom, 2.0);
1779        assert_eq!(s.min_zoom, 0.1);
1780        assert_eq!(s.line_visible_zoom, 0.2);
1781        assert_eq!(s.label_visible_zoom, 0.58);
1782        assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1783        assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1784        assert_eq!(s.node_text_size, 12.0);
1785        assert_eq!(s.label_text_size, 24.0);
1786        // light + dark themes
1787        assert_eq!(s.styles.len(), 2);
1788        // light theme
1789        assert_eq!(s.styles[0].background_color, Color32::WHITE);
1790        assert!(s.styles[0].border.is_some());
1791        assert!(s.styles[0].line.is_some());
1792        assert!(s.styles[0].font.is_some());
1793        // dark theme
1794        assert_eq!(s.styles[1].background_color, Color32::DARK_GRAY);
1795        assert!(s.styles[1].border.is_some());
1796        assert!(s.styles[1].line.is_some());
1797        assert!(s.styles[1].font.is_some());
1798    }
1799
1800    // ---------- VisibilitySetting ----------
1801
1802    #[test]
1803    fn visibility_setting_equality() {
1804        assert_eq!(VisibilitySetting::Hidden, VisibilitySetting::Hidden);
1805        assert_eq!(VisibilitySetting::Hover, VisibilitySetting::Hover);
1806        assert_eq!(VisibilitySetting::Always, VisibilitySetting::Always);
1807        assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Hover);
1808        assert_ne!(VisibilitySetting::Hover, VisibilitySetting::Always);
1809        assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Always);
1810    }
1811}