Skip to main content

egui_map_view/layers/
area.rs

1//! A layer for placing polygons on the map.
2//!
3//! # Example
4//!
5//! ```no_run
6//! use eframe::egui;
7//! use egui_map_view::{Map, config::OpenStreetMapConfig, layers::{area::{Area, AreaLayer, AreaMode, AreaShape::Polygon, FillType}, Layer}, projection::GeoPos};
8//! use egui::{Color32, Stroke};
9//!
10//! struct MyApp {
11//!     map: Map,
12//! }
13//!
14//! impl Default for MyApp {
15//!   fn default() -> Self {
16//!     let mut map = Map::new(OpenStreetMapConfig::default());
17//!
18//!     let mut area_layer = AreaLayer::default();
19//!     area_layer.add_area(Area {
20//!         shape: Polygon(vec![
21//!             GeoPos { lon: 10.0, lat: 55.0 },
22//!             GeoPos { lon: 11.0, lat: 55.0 },
23//!             GeoPos { lon: 10.5, lat: 55.5 },
24//!         ]),
25//!         stroke: Stroke::new(2.0, Color32::from_rgb(255, 0, 0)),
26//!         fill: Color32::from_rgba_unmultiplied(255, 0, 0, 50),
27//!         fill_type: FillType::Solid,
28//!     });
29//!     area_layer.mode = AreaMode::Modify;
30//!
31//!     map.add_layer("areas", area_layer);
32//!
33//!     Self { map }
34//!   }
35//! }
36//!
37//! impl eframe::App for MyApp {
38//!     fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
39//!         egui::CentralPanel::default().show_inside(ui, |ui| {
40//!             ui.add(&mut self.map);
41//!         });
42//!     }
43//! }
44//! ```
45
46use crate::layers::{
47    Layer, default_opacity, dist_sq_to_segment, projection_factor, segments_intersect,
48    serde_color32, serde_stroke,
49};
50use crate::projection::{GeoPos, MapProjection};
51use egui::{Color32, Mesh, Painter, Pos2, Response, Shape, Stroke};
52use log::warn;
53use serde::{Deserialize, Serialize};
54use std::any::Any;
55
56/// The mode of the `AreaLayer`.
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum AreaMode {
59    /// The layer is not interactive.
60    #[default]
61    Disabled,
62    /// All areas and their nodes are interactive.
63    Modify,
64    /// Only the selected area is interactive.
65    ModifySelected,
66}
67
68/// The shape of a polygon area on the map.
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub enum AreaShape {
71    /// A freeform polygon defined by a list of points.
72    Polygon(Vec<GeoPos>),
73    /// A circle defined by its center and radius in meters.
74    Circle {
75        /// The geographical center of the circle.
76        center: GeoPos,
77        /// The radius of the circle in meters.
78        radius: f64,
79        /// How many points should be used to draw the circle. If None the the point count is determined automatically which might look edged depending on zoom and projection.
80        points: Option<i64>,
81    },
82    /// An ellipse defined by its center, major and minor radii in meters, and rotation in radians.
83    Ellipse {
84        /// The geographical center of the ellipse.
85        center: GeoPos,
86        /// The semi-major axis (radius) of the ellipse in meters.
87        radius_major: f64,
88        /// The semi-minor axis (radius) of the ellipse in meters.
89        radius_minor: f64,
90        /// The rotation of the ellipse in radians, measured counter-clockwise from the East (positive X-axis).
91        rotation: f64,
92        /// How many points should be used to draw the ellipse. If None, the point count is determined automatically.
93        points: Option<i64>,
94    },
95}
96
97/// How the interior of an area is filled.
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
99pub enum FillType {
100    /// No fill — only the outline is drawn.
101    None,
102    /// Solid color fill.
103    #[default]
104    Solid,
105    /// Diagonal hatching lines using the stroke style.
106    Hatching,
107}
108
109/// A polygon area on the map.
110#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct Area {
112    /// The shape of the area.
113    pub shape: AreaShape,
114
115    /// The stroke style for drawing the polygon outlines.
116    #[serde(with = "serde_stroke")]
117    pub stroke: Stroke,
118
119    /// The fill color of the polygon.
120    #[serde(with = "serde_color32")]
121    pub fill: Color32,
122
123    /// How the interior of the area is filled.
124    #[serde(default)]
125    pub fill_type: FillType,
126}
127
128/// Represents what part of an area is being dragged.
129#[derive(Clone, Debug)]
130enum DraggedObject {
131    PolygonNode {
132        area_index: usize,
133        node_index: usize,
134    },
135    CircleCenter {
136        area_index: usize,
137    },
138    CircleRadius {
139        area_index: usize,
140    },
141    EllipseCenter {
142        area_index: usize,
143    },
144    EllipseMajorRadius {
145        area_index: usize,
146    },
147    EllipseMinorRadius {
148        area_index: usize,
149    },
150    EllipseRotation {
151        area_index: usize,
152    },
153}
154
155/// Layer implementation that allows the user to draw polygons on the map.
156#[derive(Clone, Serialize, Deserialize)]
157#[serde(default)]
158pub struct AreaLayer {
159    areas: Vec<Area>,
160
161    #[serde(skip)]
162    /// The radius of the nodes.
163    pub node_radius: f32,
164
165    #[serde(skip)]
166    /// The fill color of the nodes.
167    pub node_fill: Color32,
168
169    #[serde(skip)]
170    /// The current drawing mode.
171    pub mode: AreaMode,
172
173    #[serde(skip)]
174    dragged_object: Option<DraggedObject>,
175
176    #[serde(skip)]
177    hovered_object: Option<DraggedObject>,
178
179    /// The opacity of the layer.
180    #[serde(default = "default_opacity")]
181    pub opacity: f32,
182
183    #[serde(skip)]
184    /// The index of the currently selected area. Only used when in `AreaMode::Selected`.
185    pub selected_area: Option<usize>,
186}
187
188impl Default for AreaLayer {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194impl AreaLayer {
195    /// Creates a new `AreaLayer`.
196    #[must_use]
197    pub fn new() -> Self {
198        Self {
199            areas: Vec::new(),
200            node_radius: 5.0,
201            node_fill: Color32::from_rgb(0, 128, 0),
202            mode: AreaMode::default(),
203            dragged_object: None,
204            hovered_object: None,
205            opacity: 1.0,
206            selected_area: None,
207        }
208    }
209
210    /// Adds a new area to the layer.
211    pub fn add_area(&mut self, area: Area) {
212        self.areas.push(area);
213    }
214
215    /// Returns a reference to the areas in the layer.
216    #[must_use]
217    pub fn areas(&self) -> &Vec<Area> {
218        &self.areas
219    }
220
221    /// Returns a mutable reference to the areas in the layer.
222    pub fn areas_mut(&mut self) -> &mut Vec<Area> {
223        &mut self.areas
224    }
225
226    /// Serializes the layer to a `GeoJSON` `FeatureCollection`.
227    #[cfg(feature = "geojson")]
228    pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
229        let features: Vec<geojson::Feature> = self
230            .areas
231            .clone()
232            .into_iter()
233            .map(geojson::Feature::from)
234            .collect();
235        let mut foreign_members = serde_json::Map::new();
236        foreign_members.insert(
237            "opacity".to_string(),
238            serde_json::Value::from(f64::from(self.opacity)),
239        );
240
241        let feature_collection = geojson::FeatureCollection {
242            bbox: None,
243            features,
244            foreign_members: Some(foreign_members),
245        };
246        serde_json::to_string(&feature_collection)
247    }
248
249    /// Deserializes a `GeoJSON` `FeatureCollection` and adds the features to the layer.
250    #[cfg(feature = "geojson")]
251    pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
252        let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
253        let new_areas: Vec<Area> = feature_collection
254            .features
255            .into_iter()
256            .filter_map(|f| Area::try_from(f).ok())
257            .collect();
258        self.areas.extend(new_areas);
259
260        if let Some(foreign_members) = feature_collection.foreign_members
261            && let Some(value) = foreign_members.get("opacity")
262            && let Some(opacity) = value.as_f64()
263        {
264            self.opacity = opacity as f32;
265        }
266        Ok(())
267    }
268
269    fn handle_modify_input(
270        &mut self,
271        response: &Response,
272        projection: &MapProjection,
273        limit_to_area: Option<usize>,
274    ) -> bool {
275        self.hovered_object = response
276            .hover_pos()
277            .and_then(|pos| self.find_object_at(pos, projection, limit_to_area));
278
279        if response.double_clicked()
280            && let Some(pointer_pos) = response.interact_pointer_pos()
281        {
282            // TODO: This only works for polygons.
283            if self
284                .find_node_at(pointer_pos, projection, limit_to_area)
285                .is_none()
286                && let Some((area_idx, node_idx)) =
287                    self.find_line_segment_at(pointer_pos, projection, limit_to_area)
288                && let Some(area) = self.areas.get_mut(area_idx)
289                && let AreaShape::Polygon(points) = &mut area.shape
290            {
291                let p1_screen = projection.project(points[node_idx]);
292                let p2_screen = projection.project(points[(node_idx + 1) % points.len()]);
293
294                let t = projection_factor(pointer_pos, p1_screen, p2_screen);
295
296                // Interpolate in screen space and unproject to get the new geographical position.
297                let new_pos_screen = p1_screen.lerp(p2_screen, t);
298                let new_pos_geo = projection.unproject(new_pos_screen);
299
300                points.insert(node_idx + 1, new_pos_geo);
301
302                // This interaction is fully handled, so we can return.
303                return response.hovered();
304            }
305        }
306
307        if response.drag_started()
308            && let Some(pointer_pos) = response.interact_pointer_pos()
309        {
310            self.dragged_object = self.find_object_at(pointer_pos, projection, limit_to_area);
311        }
312
313        if response.dragged()
314            && let Some(dragged_object) = self.dragged_object.clone()
315            && let Some(pointer_pos) = response.interact_pointer_pos()
316        {
317            match dragged_object {
318                DraggedObject::PolygonNode {
319                    area_index,
320                    node_index,
321                } => {
322                    if self.is_move_valid(area_index, node_index, pointer_pos, projection)
323                        && let Some(area) = self.areas.get_mut(area_index)
324                    {
325                        let mut revert_info = None;
326                        if let AreaShape::Polygon(points) = &mut area.shape
327                            && let Some(node) = points.get_mut(node_index)
328                        {
329                            let old_pos = *node;
330                            *node = projection.unproject(pointer_pos);
331                            revert_info = Some(old_pos);
332                        }
333
334                        if let Some(old_pos) = revert_info
335                            && !area.can_triangulate(projection)
336                        {
337                            warn!("Triangulation failed, cancelling drag");
338                            self.dragged_object = None;
339                            if let AreaShape::Polygon(points) = &mut area.shape {
340                                points[node_index] = old_pos;
341                            }
342                        }
343                    }
344                }
345                DraggedObject::CircleCenter { area_index } => {
346                    if let Some(area) = self.areas.get_mut(area_index) {
347                        let mut revert_center = None;
348                        if let AreaShape::Circle { center, .. } = &mut area.shape {
349                            revert_center = Some(*center);
350                            *center = projection.unproject(pointer_pos);
351                        }
352
353                        if let Some(old_center) = revert_center
354                            && !area.can_triangulate(projection)
355                        {
356                            warn!("Triangulation failed, cancelling drag");
357                            self.dragged_object = None;
358                            if let AreaShape::Circle { center, .. } = &mut area.shape {
359                                *center = old_center;
360                            }
361                        }
362                    }
363                }
364                DraggedObject::CircleRadius { area_index } => {
365                    if let Some(area) = self.areas.get_mut(area_index) {
366                        let mut revert_radius = None;
367                        if let AreaShape::Circle {
368                            center,
369                            radius,
370                            points: _,
371                        } = &mut area.shape
372                        {
373                            revert_radius = Some(*radius);
374                            // Convert the new screen-space radius back to meters.
375                            let center_screen = projection.project(*center);
376                            let new_radius_pixels = pointer_pos.distance(center_screen);
377                            let new_edge_screen =
378                                center_screen + egui::vec2(new_radius_pixels, 0.0);
379                            let new_edge_geo = projection.unproject(new_edge_screen);
380
381                            // Calculate distance in meters. This is an approximation that works well for smaller distances.
382                            let distance_lon = (new_edge_geo.lon - center.lon).abs()
383                                * (111_320.0 * center.lat.to_radians().cos().max(1e-6));
384                            let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
385                            let new_val = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
386                            if new_val.is_finite() {
387                                *radius = new_val;
388                            }
389                        }
390
391                        if let Some(old_radius) = revert_radius
392                            && !area.can_triangulate(projection)
393                        {
394                            warn!("Triangulation failed, cancelling drag");
395                            self.dragged_object = None;
396                            if let AreaShape::Circle { radius, .. } = &mut area.shape {
397                                *radius = old_radius;
398                            }
399                        }
400                    }
401                }
402                DraggedObject::EllipseCenter { area_index } => {
403                    if let Some(area) = self.areas.get_mut(area_index) {
404                        let mut revert_center = None;
405                        if let AreaShape::Ellipse { center, .. } = &mut area.shape {
406                            revert_center = Some(*center);
407                            *center = projection.unproject(pointer_pos);
408                        }
409
410                        if let Some(old_center) = revert_center
411                            && !area.can_triangulate(projection)
412                        {
413                            warn!("Triangulation failed, cancelling drag");
414                            self.dragged_object = None;
415                            if let AreaShape::Ellipse { center, .. } = &mut area.shape {
416                                *center = old_center;
417                            }
418                        }
419                    }
420                }
421                DraggedObject::EllipseMajorRadius { area_index } => {
422                    if let Some(area) = self.areas.get_mut(area_index) {
423                        let mut revert_radius_major = None;
424                        if let AreaShape::Ellipse {
425                            center,
426                            radius_major,
427                            rotation,
428                            ..
429                        } = &mut area.shape
430                        {
431                            revert_radius_major = Some(*radius_major);
432                            let center_screen = projection.project(*center);
433                            let cos_rot = rotation.cos() as f32;
434                            let sin_rot = rotation.sin() as f32;
435                            let v_major = egui::vec2(cos_rot, sin_rot);
436                            let new_radius_pixels =
437                                (pointer_pos - center_screen).dot(v_major).max(1.0);
438                            let new_edge_screen =
439                                center_screen + egui::vec2(new_radius_pixels, 0.0);
440                            let new_edge_geo = projection.unproject(new_edge_screen);
441                            let distance_lon = (new_edge_geo.lon - center.lon).abs()
442                                * (111_320.0 * center.lat.to_radians().cos().max(1e-6));
443                            let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
444                            let new_val = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
445                            if new_val.is_finite() {
446                                *radius_major = new_val;
447                            }
448                        }
449
450                        if let Some(old_radius) = revert_radius_major
451                            && !area.can_triangulate(projection)
452                        {
453                            warn!("Triangulation failed, cancelling drag");
454                            self.dragged_object = None;
455                            if let AreaShape::Ellipse { radius_major, .. } = &mut area.shape {
456                                *radius_major = old_radius;
457                            }
458                        }
459                    }
460                }
461                DraggedObject::EllipseMinorRadius { area_index } => {
462                    if let Some(area) = self.areas.get_mut(area_index) {
463                        let mut revert_radius_minor = None;
464                        if let AreaShape::Ellipse {
465                            center,
466                            radius_minor,
467                            rotation,
468                            ..
469                        } = &mut area.shape
470                        {
471                            revert_radius_minor = Some(*radius_minor);
472                            let center_screen = projection.project(*center);
473                            let cos_rot = rotation.cos() as f32;
474                            let sin_rot = rotation.sin() as f32;
475                            let v_minor = egui::vec2(-sin_rot, cos_rot);
476                            let new_radius_pixels =
477                                (pointer_pos - center_screen).dot(v_minor).max(1.0);
478                            let new_edge_screen =
479                                center_screen + egui::vec2(new_radius_pixels, 0.0);
480                            let new_edge_geo = projection.unproject(new_edge_screen);
481                            let distance_lon = (new_edge_geo.lon - center.lon).abs()
482                                * (111_320.0 * center.lat.to_radians().cos().max(1e-6));
483                            let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
484                            let new_val = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
485                            if new_val.is_finite() {
486                                *radius_minor = new_val;
487                            }
488                        }
489
490                        if let Some(old_radius) = revert_radius_minor
491                            && !area.can_triangulate(projection)
492                        {
493                            warn!("Triangulation failed, cancelling drag");
494                            self.dragged_object = None;
495                            if let AreaShape::Ellipse { radius_minor, .. } = &mut area.shape {
496                                *radius_minor = old_radius;
497                            }
498                        }
499                    }
500                }
501                DraggedObject::EllipseRotation { area_index } => {
502                    if let Some(area) = self.areas.get_mut(area_index) {
503                        let mut revert_rotation = None;
504                        if let AreaShape::Ellipse {
505                            center, rotation, ..
506                        } = &mut area.shape
507                        {
508                            revert_rotation = Some(*rotation);
509                            let center_screen = projection.project(*center);
510                            let new_val = f64::from(
511                                (pointer_pos - center_screen)
512                                    .y
513                                    .atan2((pointer_pos - center_screen).x),
514                            );
515                            if new_val.is_finite() {
516                                *rotation = new_val;
517                            }
518                        }
519
520                        if let Some(old_rotation) = revert_rotation
521                            && !area.can_triangulate(projection)
522                        {
523                            warn!("Triangulation failed, cancelling drag");
524                            self.dragged_object = None;
525                            if let AreaShape::Ellipse { rotation, .. } = &mut area.shape {
526                                *rotation = old_rotation;
527                            }
528                        }
529                    }
530                }
531            }
532        }
533
534        if response.drag_stopped() {
535            self.dragged_object = None;
536        }
537
538        let is_dragging = self.dragged_object.is_some();
539
540        if is_dragging {
541            response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
542        } else if self.hovered_object.is_some() {
543            response.ctx.set_cursor_icon(egui::CursorIcon::Grab);
544        }
545
546        is_dragging || (response.hovered() && self.hovered_object.is_some())
547    }
548
549    fn find_object_at(
550        &self,
551        screen_pos: Pos2,
552        projection: &MapProjection,
553        limit_to_area: Option<usize>,
554    ) -> Option<DraggedObject> {
555        let click_tolerance_sq = (self.node_radius * 3.0).powi(2);
556
557        for (area_idx, area) in self.areas.iter().enumerate().rev() {
558            if let Some(limit_idx) = limit_to_area
559                && area_idx != limit_idx
560            {
561                continue;
562            }
563            match &area.shape {
564                AreaShape::Polygon(points) => {
565                    for (node_idx, node) in points.iter().enumerate() {
566                        let node_screen_pos = projection.project(*node);
567                        if node_screen_pos.distance_sq(screen_pos) < click_tolerance_sq {
568                            return Some(DraggedObject::PolygonNode {
569                                area_index: area_idx,
570                                node_index: node_idx,
571                            });
572                        }
573                    }
574                }
575                AreaShape::Circle {
576                    center,
577                    radius,
578                    points: _,
579                } => {
580                    let center_screen = projection.project(*center);
581
582                    // Convert radius from meters to screen pixels to correctly detect handle clicks.
583                    let point_on_circle_geo = GeoPos {
584                        lon: center.lon
585                            + (radius / (111_320.0 * center.lat.to_radians().cos().max(1e-6))),
586                        lat: center.lat,
587                    };
588                    let point_on_circle_screen = projection.project(point_on_circle_geo);
589                    let radius_pixels = center_screen.distance(point_on_circle_screen);
590
591                    // Check for radius handle
592                    let distance_to_edge =
593                        (center_screen.distance(screen_pos) - radius_pixels).abs();
594                    if distance_to_edge < self.node_radius * 2.0 {
595                        return Some(DraggedObject::CircleRadius {
596                            area_index: area_idx,
597                        });
598                    }
599
600                    // Check for center
601                    if center_screen.distance_sq(screen_pos) < click_tolerance_sq {
602                        return Some(DraggedObject::CircleCenter {
603                            area_index: area_idx,
604                        });
605                    }
606                }
607                AreaShape::Ellipse {
608                    center,
609                    radius_major,
610                    radius_minor,
611                    rotation,
612                    points: _,
613                } => {
614                    let center_geo = *center;
615                    let point_on_major_geo = GeoPos {
616                        lon: center_geo.lon
617                            + (radius_major
618                                / (111_320.0 * center_geo.lat.to_radians().cos().max(1e-6))),
619                        lat: center_geo.lat,
620                    };
621                    let point_on_minor_geo = GeoPos {
622                        lon: center_geo.lon,
623                        lat: center_geo.lat + (radius_minor / 110_574.0),
624                    };
625                    let center_screen = projection.project(center_geo);
626                    let point_on_major_screen = projection.project(point_on_major_geo);
627                    let point_on_minor_screen = projection.project(point_on_minor_geo);
628                    let radius_major_pixels = center_screen.distance(point_on_major_screen);
629                    let radius_minor_pixels = center_screen.distance(point_on_minor_screen);
630
631                    let cos_rot = rotation.cos() as f32;
632                    let sin_rot = rotation.sin() as f32;
633
634                    // Rotation Handle: checked first
635                    let rotation_handle_pos = center_screen
636                        + egui::vec2(
637                            (radius_major_pixels + 20.0) * cos_rot,
638                            (radius_major_pixels + 20.0) * sin_rot,
639                        );
640                    if rotation_handle_pos.distance_sq(screen_pos) < click_tolerance_sq {
641                        return Some(DraggedObject::EllipseRotation {
642                            area_index: area_idx,
643                        });
644                    }
645
646                    // Major Radius Handle
647                    let major_handle_pos = center_screen
648                        + egui::vec2(radius_major_pixels * cos_rot, radius_major_pixels * sin_rot);
649                    if major_handle_pos.distance_sq(screen_pos) < click_tolerance_sq {
650                        return Some(DraggedObject::EllipseMajorRadius {
651                            area_index: area_idx,
652                        });
653                    }
654
655                    // Minor Radius Handle
656                    let minor_handle_pos = center_screen
657                        + egui::vec2(
658                            -radius_minor_pixels * sin_rot,
659                            radius_minor_pixels * cos_rot,
660                        );
661                    if minor_handle_pos.distance_sq(screen_pos) < click_tolerance_sq {
662                        return Some(DraggedObject::EllipseMinorRadius {
663                            area_index: area_idx,
664                        });
665                    }
666
667                    // Center Handle
668                    if center_screen.distance_sq(screen_pos) < click_tolerance_sq {
669                        return Some(DraggedObject::EllipseCenter {
670                            area_index: area_idx,
671                        });
672                    }
673                }
674            }
675        }
676
677        None
678    }
679
680    fn find_node_at(
681        &self,
682        screen_pos: Pos2,
683        projection: &MapProjection,
684        limit_to_area: Option<usize>,
685    ) -> Option<(usize, usize)> {
686        match self.find_object_at(screen_pos, projection, limit_to_area) {
687            Some(DraggedObject::PolygonNode {
688                area_index,
689                node_index,
690            }) => Some((area_index, node_index)),
691            _ => None,
692        }
693    }
694
695    fn find_line_segment_at(
696        &self,
697        screen_pos: Pos2,
698        projection: &MapProjection,
699        limit_to_area: Option<usize>,
700    ) -> Option<(usize, usize)> {
701        let click_tolerance = (self.node_radius * 2.0).powi(2);
702
703        for (area_idx, area) in self.areas.iter().enumerate().rev() {
704            if let Some(limit_idx) = limit_to_area
705                && area_idx != limit_idx
706            {
707                continue;
708            }
709            if let AreaShape::Polygon(points) = &area.shape {
710                if points.len() < 2 {
711                    continue;
712                }
713                for i in 0..points.len() {
714                    let p1 = projection.project(points[i]);
715                    let p2 = projection.project(points[(i + 1) % points.len()]);
716
717                    if dist_sq_to_segment(screen_pos, p1, p2) < click_tolerance {
718                        return Some((area_idx, i));
719                    }
720                }
721            }
722        }
723        None
724    }
725
726    /// Checks if moving a node to a new position would cause the polygon to self-intersect.
727    fn is_move_valid(
728        &self,
729        area_idx: usize,
730        node_idx: usize,
731        new_screen_pos: Pos2,
732        projection: &MapProjection,
733    ) -> bool {
734        let area = if let Some(area) = self.areas.get(area_idx) {
735            area
736        } else {
737            return false; // TODO: Should not happen
738        };
739
740        let points = match &area.shape {
741            AreaShape::Polygon(points) => points,
742            _ => return true, // Not a polygon, no intersections possible.
743        };
744
745        if points.len() < 3 {
746            return true;
747        }
748        let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
749
750        let n = screen_points.len();
751        let prev_node_idx = (node_idx + n - 1) % n;
752        let next_node_idx = (node_idx + 1) % n;
753
754        // The two edges that are being modified by the drag.
755        let new_edge1 = (screen_points[prev_node_idx], new_screen_pos);
756        let new_edge2 = (new_screen_pos, screen_points[next_node_idx]);
757
758        for i in 0..n {
759            let p1_idx = i;
760            let p2_idx = (i + 1) % n;
761
762            // Don't check against the edges connected to the dragged node.
763            if p1_idx == node_idx || p2_idx == node_idx {
764                continue;
765            }
766
767            let edge_to_check = (screen_points[p1_idx], screen_points[p2_idx]);
768
769            // Check against the first new edge.
770            if p1_idx != prev_node_idx
771                && p2_idx != prev_node_idx
772                && segments_intersect(new_edge1.0, new_edge1.1, edge_to_check.0, edge_to_check.1)
773            {
774                return false;
775            }
776
777            // Check against the second new edge.
778            if p1_idx != next_node_idx
779                && p2_idx != next_node_idx
780                && segments_intersect(new_edge2.0, new_edge2.1, edge_to_check.0, edge_to_check.1)
781            {
782                return false;
783            }
784        }
785
786        true
787    }
788}
789
790impl Area {
791    /// Checks if the area can be successfully triangulated.
792    fn can_triangulate(&self, projection: &MapProjection) -> bool {
793        let points = self.get_points(projection);
794        let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
795
796        if screen_points.len() < 3 {
797            return true;
798        }
799
800        let flat_points: Vec<f64> = screen_points
801            .iter()
802            .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
803            .collect();
804        earcutr::earcut(&flat_points, &[], 2).is_ok()
805    }
806
807    /// Returns the points of the area. For a circle, it generates a polygon approximation.
808    fn get_points(&self, projection: &MapProjection) -> Vec<GeoPos> {
809        match &self.shape {
810            AreaShape::Polygon(points) => points.clone(),
811            AreaShape::Circle {
812                center,
813                radius,
814                points,
815            } => {
816                // Convert radius from meters to screen pixels.
817                let center_geo = *center;
818                let point_on_circle_geo = GeoPos {
819                    lon: center_geo.lon
820                        + (radius / (111_320.0 * center_geo.lat.to_radians().cos().max(1e-6))),
821                    lat: center_geo.lat,
822                };
823                let center_screen = projection.project(center_geo);
824                let point_on_circle_screen = projection.project(point_on_circle_geo);
825                let radius_pixels = center_screen.distance(point_on_circle_screen);
826
827                let num_points = if let Some(points) = points {
828                    *points
829                } else {
830                    // Automatically determine the number of points based on the circle's radius
831                    // to ensure it looks smooth.
832                    (f64::from(radius_pixels) * 2.0 * std::f64::consts::PI / 10.0).ceil() as i64
833                };
834                let num_points = num_points.max(3);
835                let mut circle_points = Vec::with_capacity(num_points as usize);
836
837                for i in 0..num_points {
838                    let angle = (i as f64 / num_points as f64) * 2.0 * std::f64::consts::PI;
839                    let point_screen = center_screen
840                        + egui::vec2(
841                            radius_pixels * angle.cos() as f32,
842                            radius_pixels * angle.sin() as f32,
843                        );
844                    circle_points.push(projection.unproject(point_screen));
845                }
846                circle_points
847            }
848            AreaShape::Ellipse {
849                center,
850                radius_major,
851                radius_minor,
852                rotation,
853                points,
854            } => {
855                let center_geo = *center;
856                let point_on_major_geo = GeoPos {
857                    lon: center_geo.lon
858                        + (radius_major
859                            / (111_320.0 * center_geo.lat.to_radians().cos().max(1e-6))),
860                    lat: center_geo.lat,
861                };
862                let point_on_minor_geo = GeoPos {
863                    lon: center_geo.lon,
864                    lat: center_geo.lat + (radius_minor / 110_574.0),
865                };
866                let center_screen = projection.project(center_geo);
867                let point_on_major_screen = projection.project(point_on_major_geo);
868                let point_on_minor_screen = projection.project(point_on_minor_geo);
869                let radius_major_pixels = center_screen.distance(point_on_major_screen);
870                let radius_minor_pixels = center_screen.distance(point_on_minor_screen);
871
872                let num_points = if let Some(points) = points {
873                    *points
874                } else {
875                    let max_radius = radius_major_pixels.max(radius_minor_pixels);
876                    (f64::from(max_radius) * 2.0 * std::f64::consts::PI / 10.0).ceil() as i64
877                };
878                let num_points = num_points.max(3);
879                let mut ellipse_points = Vec::with_capacity(num_points as usize);
880                let cos_rot = rotation.cos();
881                let sin_rot = rotation.sin();
882
883                for i in 0..num_points {
884                    let angle = (i as f64 / num_points as f64) * 2.0 * std::f64::consts::PI;
885                    let dx = f64::from(radius_major_pixels) * angle.cos();
886                    let dy = f64::from(radius_minor_pixels) * angle.sin();
887                    let rx = dx * cos_rot - dy * sin_rot;
888                    let ry = dx * sin_rot + dy * cos_rot;
889                    let point_screen = center_screen + egui::vec2(rx as f32, ry as f32);
890                    ellipse_points.push(projection.unproject(point_screen));
891                }
892                ellipse_points
893            }
894        }
895    }
896
897    /// Checks if a screen position is inside the area.
898    pub fn contains(&self, pos: Pos2, projection: &MapProjection) -> bool {
899        match &self.shape {
900            AreaShape::Circle { center, radius, .. } => {
901                let center_screen = projection.project(*center);
902                let point_on_circle_geo = GeoPos {
903                    lon: center.lon
904                        + (radius / (111_320.0 * center.lat.to_radians().cos().max(1e-6))),
905                    lat: center.lat,
906                };
907                let point_on_circle_screen = projection.project(point_on_circle_geo);
908                let radius_pixels = center_screen.distance(point_on_circle_screen);
909                if radius_pixels <= 0.0 {
910                    return false;
911                }
912                center_screen.distance_sq(pos) <= radius_pixels.powi(2)
913            }
914            AreaShape::Ellipse {
915                center,
916                radius_major,
917                radius_minor,
918                rotation,
919                ..
920            } => {
921                let center_geo = *center;
922                let point_on_major_geo = GeoPos {
923                    lon: center_geo.lon
924                        + (radius_major
925                            / (111_320.0 * center_geo.lat.to_radians().cos().max(1e-6))),
926                    lat: center_geo.lat,
927                };
928                let point_on_minor_geo = GeoPos {
929                    lon: center_geo.lon,
930                    lat: center_geo.lat + (radius_minor / 110_574.0),
931                };
932                let center_screen = projection.project(center_geo);
933                let point_on_major_screen = projection.project(point_on_major_geo);
934                let point_on_minor_screen = projection.project(point_on_minor_geo);
935                let radius_major_pixels = center_screen.distance(point_on_major_screen);
936                let radius_minor_pixels = center_screen.distance(point_on_minor_screen);
937
938                if radius_major_pixels <= 0.0 || radius_minor_pixels <= 0.0 {
939                    return false;
940                }
941
942                let v = pos - center_screen;
943                let cos_rot = rotation.cos() as f32;
944                let sin_rot = rotation.sin() as f32;
945                let local_x = v.x * cos_rot + v.y * sin_rot;
946                let local_y = -v.x * sin_rot + v.y * cos_rot;
947
948                let rx_f64 = f64::from(radius_major_pixels);
949                let ry_f64 = f64::from(radius_minor_pixels);
950                (f64::from(local_x) / rx_f64).powi(2) + (f64::from(local_y) / ry_f64).powi(2) <= 1.0
951            }
952            AreaShape::Polygon(_) => {
953                let points = self.get_points(projection);
954                let screen_points: Vec<Pos2> =
955                    points.iter().map(|p| projection.project(*p)).collect();
956                if screen_points.len() < 3 {
957                    return false;
958                }
959                let flat_points: Vec<f64> = screen_points
960                    .iter()
961                    .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
962                    .collect();
963                if let Ok(indices) = earcutr::earcut(&flat_points, &[], 2) {
964                    for chunk in indices.chunks_exact(3) {
965                        let p1 = screen_points[chunk[0]];
966                        let p2 = screen_points[chunk[1]];
967                        let p3 = screen_points[chunk[2]];
968                        if point_in_triangle(pos, p1, p2, p3) {
969                            return true;
970                        }
971                    }
972                }
973                false
974            }
975        }
976    }
977}
978
979fn point_in_triangle(p: Pos2, a: Pos2, b: Pos2, c: Pos2) -> bool {
980    let d1 = sign(p, a, b);
981    let d2 = sign(p, b, c);
982    let d3 = sign(p, c, a);
983
984    let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
985    let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
986
987    !(has_neg && has_pos)
988}
989
990fn sign(p1: Pos2, p2: Pos2, p3: Pos2) -> f32 {
991    (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
992}
993
994/// Generates diagonal hatching line segments clipped to the given polygon.
995///
996/// `screen_points` are the polygon vertices in screen space (must be >= 3 points).
997/// `spacing` is the distance in pixels between parallel hatching lines.
998/// `angle` is the angle of the hatching lines in radians (0 = horizontal, PI/4 = 45° diagonal).
999///
1000/// Returns a list of line segments `(start, end)` that lie inside the polygon.
1001fn generate_hatching_lines(screen_points: &[Pos2], spacing: f32, angle: f32) -> Vec<(Pos2, Pos2)> {
1002    if screen_points.len() < 3 || spacing <= 0.0 {
1003        return Vec::new();
1004    }
1005
1006    // Direction along the hatching lines and perpendicular to them.
1007    let dir = egui::vec2(angle.cos(), angle.sin());
1008    let perp = egui::vec2(-angle.sin(), angle.cos());
1009
1010    // Project all polygon points onto the perpendicular axis to find the sweep range.
1011    let mut min_perp = f32::MAX;
1012    let mut max_perp = f32::MIN;
1013    for p in screen_points {
1014        let d = p.to_vec2().dot(perp);
1015        min_perp = min_perp.min(d);
1016        max_perp = max_perp.max(d);
1017    }
1018
1019    let n = screen_points.len();
1020    let mut segments = Vec::new();
1021
1022    // Sweep parallel lines across the polygon.
1023    let mut offset = min_perp + spacing;
1024    while offset < max_perp {
1025        // A point on the current sweep line: origin + offset along the perpendicular.
1026        let line_origin = Pos2::ZERO + perp * offset;
1027
1028        // Find intersections of this sweep line with every polygon edge.
1029        let mut t_values: Vec<f32> = Vec::new();
1030        for i in 0..n {
1031            let a = screen_points[i];
1032            let b = screen_points[(i + 1) % n];
1033            let edge = b - a;
1034
1035            // Solve: a + t_edge * edge = line_origin + t_line * dir
1036            // Cross product form: (a - line_origin) × dir = t_edge * (edge × dir)
1037            let denom = edge.x * dir.y - edge.y * dir.x;
1038            if denom.abs() < 1e-9 {
1039                continue; // Edge is parallel to the hatching line.
1040            }
1041
1042            let diff = a - line_origin;
1043            let t_edge = -(diff.x * dir.y - diff.y * dir.x) / denom;
1044
1045            if (0.0..=1.0).contains(&t_edge) {
1046                // Compute t_line: the parameter along the sweep line direction.
1047                let t_line = if dir.x.abs() > dir.y.abs() {
1048                    (a.x - line_origin.x + t_edge * edge.x) / dir.x
1049                } else {
1050                    (a.y - line_origin.y + t_edge * edge.y) / dir.y
1051                };
1052                t_values.push(t_line);
1053            }
1054        }
1055
1056        // Sort intersections along the sweep line.
1057        t_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1058
1059        // Pair up intersections (even-odd rule) to get interior segments.
1060        for pair in t_values.chunks_exact(2) {
1061            let p1 = line_origin + dir * pair[0];
1062            let p2 = line_origin + dir * pair[1];
1063            segments.push((p1, p2));
1064        }
1065
1066        offset += spacing;
1067    }
1068
1069    segments
1070}
1071
1072impl Layer for AreaLayer {
1073    fn as_any(&self) -> &dyn Any {
1074        self
1075    }
1076
1077    fn as_any_mut(&mut self) -> &mut dyn Any {
1078        self
1079    }
1080
1081    fn opacity(&self) -> f32 {
1082        self.opacity
1083    }
1084
1085    fn set_opacity(&mut self, opacity: f32) {
1086        self.opacity = opacity;
1087    }
1088
1089    fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
1090        match self.mode {
1091            AreaMode::Disabled => {
1092                self.hovered_object = None;
1093                false
1094            }
1095            AreaMode::Modify => self.handle_modify_input(response, projection, None),
1096            AreaMode::ModifySelected => {
1097                if response.clicked()
1098                    && let Some(pointer_pos) = response.interact_pointer_pos()
1099                {
1100                    // Find if any area was clicked to select it.
1101                    let clicked_area_idx =
1102                        self.areas.iter().enumerate().rev().find_map(|(idx, area)| {
1103                            if area.contains(pointer_pos, projection) {
1104                                Some(idx)
1105                            } else {
1106                                None
1107                            }
1108                        });
1109
1110                    if clicked_area_idx != self.selected_area {
1111                        self.selected_area = clicked_area_idx;
1112                        return true;
1113                    }
1114                }
1115
1116                if let Some(selected_idx) = self.selected_area {
1117                    self.handle_modify_input(response, projection, Some(selected_idx))
1118                } else {
1119                    false
1120                }
1121            }
1122        }
1123    }
1124
1125    fn draw(&self, painter: &Painter, projection: &MapProjection) {
1126        for (area_idx, area) in self.areas.iter().enumerate() {
1127            let points = area.get_points(projection);
1128            let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
1129
1130            // Draw polygon outline
1131            if screen_points.len() >= 3 {
1132                let is_selected =
1133                    self.mode == AreaMode::ModifySelected && self.selected_area == Some(area_idx);
1134                let stroke = if is_selected {
1135                    Stroke {
1136                        width: area.stroke.width * 2.0,
1137                        color: area.stroke.color.gamma_multiply(self.opacity),
1138                    }
1139                } else {
1140                    Stroke {
1141                        color: area.stroke.color.gamma_multiply(self.opacity),
1142                        ..area.stroke
1143                    }
1144                };
1145
1146                // Use a generic path for the stroke.
1147                let path_shape = Shape::Path(egui::epaint::PathShape {
1148                    points: screen_points.clone(),
1149                    closed: true,
1150                    fill: Color32::TRANSPARENT,
1151                    stroke: stroke.into(),
1152                });
1153                painter.add(path_shape);
1154
1155                match area.fill_type {
1156                    FillType::None => { /* No fill */ }
1157                    FillType::Solid => {
1158                        // Triangulate for the fill.
1159                        let flat_points: Vec<f64> = screen_points
1160                            .iter()
1161                            .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
1162                            .collect();
1163                        match earcutr::earcut(&flat_points, &[], 2) {
1164                            Ok(indices) => {
1165                                let mesh = Mesh {
1166                                    vertices: screen_points
1167                                        .iter()
1168                                        .map(|p| egui::epaint::Vertex {
1169                                            pos: *p,
1170                                            uv: Default::default(),
1171                                            color: area.fill.gamma_multiply(self.opacity),
1172                                        })
1173                                        .collect(),
1174                                    indices: indices.into_iter().map(|i| i as u32).collect(),
1175                                    ..Default::default()
1176                                };
1177                                painter.add(Shape::Mesh(mesh.into()));
1178                            }
1179                            Err(e) => {
1180                                warn!("Failed to triangulate area: {e:?}");
1181                            }
1182                        }
1183                    }
1184                    FillType::Hatching => {
1185                        let segments = generate_hatching_lines(
1186                            &screen_points,
1187                            8.0,
1188                            std::f32::consts::FRAC_PI_4,
1189                        );
1190                        for (a, b) in segments {
1191                            painter.line_segment(
1192                                [a, b],
1193                                Stroke {
1194                                    color: area.stroke.color.gamma_multiply(self.opacity),
1195                                    ..area.stroke
1196                                },
1197                            );
1198                        }
1199                    }
1200                }
1201            } else {
1202                warn!("Invalid amount of points in area. {area:?}");
1203            }
1204
1205            // Draw nodes only when in modify mode or if specifically selected
1206            let show_nodes = self.mode == AreaMode::Modify
1207                || (self.mode == AreaMode::ModifySelected && self.selected_area == Some(area_idx));
1208            if show_nodes {
1209                match &area.shape {
1210                    AreaShape::Polygon(_) => {
1211                        for (node_idx, point) in screen_points.iter().enumerate() {
1212                            painter.circle_filled(
1213                                *point,
1214                                self.node_radius,
1215                                self.node_fill.gamma_multiply(self.opacity),
1216                            );
1217
1218                            if let Some(DraggedObject::PolygonNode {
1219                                area_index,
1220                                node_index,
1221                            }) = self.hovered_object
1222                                && area_index == area_idx
1223                                && node_index == node_idx
1224                            {
1225                                painter.circle_stroke(
1226                                    *point,
1227                                    self.node_radius * 3.0,
1228                                    Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1229                                );
1230                            }
1231                        }
1232                    }
1233                    AreaShape::Circle {
1234                        center,
1235                        radius,
1236                        points: _,
1237                    } => {
1238                        let center_screen = projection.project(*center);
1239
1240                        // Convert radius from meters to screen pixels to correctly position the handle.
1241                        let point_on_circle_geo = GeoPos {
1242                            lon: center.lon
1243                                + (radius / (111_320.0 * center.lat.to_radians().cos().max(1e-6))),
1244                            lat: center.lat,
1245                        };
1246                        let point_on_circle_screen = projection.project(point_on_circle_geo);
1247                        let radius_pixels = center_screen.distance(point_on_circle_screen);
1248
1249                        painter.circle_filled(
1250                            center_screen,
1251                            self.node_radius,
1252                            self.node_fill.gamma_multiply(self.opacity),
1253                        );
1254
1255                        if let Some(DraggedObject::CircleCenter { area_index }) =
1256                            self.hovered_object
1257                            && area_index == area_idx
1258                        {
1259                            painter.circle_stroke(
1260                                center_screen,
1261                                self.node_radius * 3.0,
1262                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1263                            );
1264                        }
1265
1266                        let radius_handle_pos = center_screen + egui::vec2(radius_pixels, 0.0);
1267                        painter.circle_filled(
1268                            radius_handle_pos,
1269                            self.node_radius,
1270                            self.node_fill.gamma_multiply(self.opacity),
1271                        );
1272
1273                        if let Some(DraggedObject::CircleRadius { area_index }) =
1274                            self.hovered_object
1275                            && area_index == area_idx
1276                        {
1277                            painter.circle_stroke(
1278                                radius_handle_pos,
1279                                self.node_radius * 2.0,
1280                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1281                            );
1282                        }
1283                    }
1284                    AreaShape::Ellipse {
1285                        center,
1286                        radius_major,
1287                        radius_minor,
1288                        rotation,
1289                        points: _,
1290                    } => {
1291                        let center_screen = projection.project(*center);
1292
1293                        let point_on_major_geo = GeoPos {
1294                            lon: center.lon
1295                                + (radius_major
1296                                    / (111_320.0 * center.lat.to_radians().cos().max(1e-6))),
1297                            lat: center.lat,
1298                        };
1299                        let point_on_minor_geo = GeoPos {
1300                            lon: center.lon,
1301                            lat: center.lat + (radius_minor / 110_574.0),
1302                        };
1303                        let point_on_major_screen = projection.project(point_on_major_geo);
1304                        let point_on_minor_screen = projection.project(point_on_minor_geo);
1305                        let radius_major_pixels = center_screen.distance(point_on_major_screen);
1306                        let radius_minor_pixels = center_screen.distance(point_on_minor_screen);
1307
1308                        let cos_rot = rotation.cos() as f32;
1309                        let sin_rot = rotation.sin() as f32;
1310
1311                        let major_handle_pos = center_screen
1312                            + egui::vec2(
1313                                radius_major_pixels * cos_rot,
1314                                radius_major_pixels * sin_rot,
1315                            );
1316                        let minor_handle_pos = center_screen
1317                            + egui::vec2(
1318                                -radius_minor_pixels * sin_rot,
1319                                radius_minor_pixels * cos_rot,
1320                            );
1321                        let rotation_handle_pos = center_screen
1322                            + egui::vec2(
1323                                (radius_major_pixels + 20.0) * cos_rot,
1324                                (radius_major_pixels + 20.0) * sin_rot,
1325                            );
1326
1327                        // Draw connection line between major handle and rotation handle.
1328                        painter.line_segment(
1329                            [major_handle_pos, rotation_handle_pos],
1330                            Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1331                        );
1332
1333                        // 1. Center
1334                        painter.circle_filled(
1335                            center_screen,
1336                            self.node_radius,
1337                            self.node_fill.gamma_multiply(self.opacity),
1338                        );
1339                        if let Some(DraggedObject::EllipseCenter { area_index }) =
1340                            self.hovered_object
1341                            && area_index == area_idx
1342                        {
1343                            painter.circle_stroke(
1344                                center_screen,
1345                                self.node_radius * 3.0,
1346                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1347                            );
1348                        }
1349
1350                        // 2. Major radius
1351                        painter.circle_filled(
1352                            major_handle_pos,
1353                            self.node_radius,
1354                            self.node_fill.gamma_multiply(self.opacity),
1355                        );
1356                        if let Some(DraggedObject::EllipseMajorRadius { area_index }) =
1357                            self.hovered_object
1358                            && area_index == area_idx
1359                        {
1360                            painter.circle_stroke(
1361                                major_handle_pos,
1362                                self.node_radius * 2.0,
1363                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1364                            );
1365                        }
1366
1367                        // 3. Minor radius
1368                        painter.circle_filled(
1369                            minor_handle_pos,
1370                            self.node_radius,
1371                            self.node_fill.gamma_multiply(self.opacity),
1372                        );
1373                        if let Some(DraggedObject::EllipseMinorRadius { area_index }) =
1374                            self.hovered_object
1375                            && area_index == area_idx
1376                        {
1377                            painter.circle_stroke(
1378                                minor_handle_pos,
1379                                self.node_radius * 2.0,
1380                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1381                            );
1382                        }
1383
1384                        // 4. Rotation
1385                        painter.circle_filled(
1386                            rotation_handle_pos,
1387                            self.node_radius,
1388                            self.node_fill.gamma_multiply(self.opacity),
1389                        );
1390                        if let Some(DraggedObject::EllipseRotation { area_index }) =
1391                            self.hovered_object
1392                            && area_index == area_idx
1393                        {
1394                            painter.circle_stroke(
1395                                rotation_handle_pos,
1396                                self.node_radius * 2.0,
1397                                Stroke::new(1.0, self.node_fill.gamma_multiply(self.opacity)),
1398                            );
1399                        }
1400                    }
1401                }
1402            }
1403        }
1404    }
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409    use super::*;
1410    use crate::projection::MapProjection;
1411    use egui::{Rect, pos2, vec2};
1412
1413    // Helper for creating a dummy projection for tests
1414    fn dummy_projection() -> MapProjection {
1415        MapProjection::new(
1416            10,                // zoom
1417            (0.0, 0.0).into(), // center
1418            Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 1000.0)),
1419        )
1420    }
1421
1422    #[test]
1423    fn area_layer_new() {
1424        let layer = AreaLayer::default();
1425        assert_eq!(layer.mode, AreaMode::Disabled);
1426        assert!(layer.areas.is_empty());
1427        assert_eq!(layer.node_radius, 5.0);
1428    }
1429
1430    #[test]
1431    fn area_layer_add_area() {
1432        let mut layer = AreaLayer::default();
1433        assert_eq!(layer.areas.len(), 0);
1434
1435        layer.add_area(Area {
1436            shape: AreaShape::Polygon(vec![
1437                (0.0, 0.0).into(),
1438                (1.0, 0.0).into(),
1439                (0.0, 1.0).into(),
1440            ]),
1441            stroke: Default::default(),
1442            fill: Default::default(),
1443            fill_type: Default::default(),
1444        });
1445
1446        assert_eq!(layer.areas.len(), 1);
1447    }
1448
1449    #[test]
1450    fn circle_get_points_with_fixed_number() {
1451        let projection = dummy_projection();
1452        let area = Area {
1453            shape: AreaShape::Circle {
1454                center: (0.0, 0.0).into(),
1455                radius: 1000.0,
1456                points: Some(16),
1457            },
1458            stroke: Default::default(),
1459            fill: Default::default(),
1460            fill_type: Default::default(),
1461        };
1462
1463        let points = area.get_points(&projection);
1464        assert_eq!(points.len(), 16);
1465    }
1466
1467    #[test]
1468    fn find_object_at_empty() {
1469        let layer = AreaLayer::default();
1470        let projection = dummy_projection();
1471        let position = pos2(100.0, 100.0);
1472
1473        assert!(layer.find_object_at(position, &projection, None).is_none());
1474    }
1475
1476    #[test]
1477    fn find_object_at_polygon_node() {
1478        let projection = dummy_projection();
1479        let mut layer = AreaLayer::default();
1480        let geo_pos = projection.unproject(pos2(100.0, 100.0));
1481
1482        layer.add_area(Area {
1483            shape: AreaShape::Polygon(vec![geo_pos]),
1484            stroke: Default::default(),
1485            fill: Default::default(),
1486            fill_type: Default::default(),
1487        });
1488
1489        // Position is exactly on the node
1490        let found = layer.find_object_at(pos2(100.0, 100.0), &projection, None);
1491        assert!(matches!(
1492            found,
1493            Some(DraggedObject::PolygonNode {
1494                area_index: 0,
1495                node_index: 0
1496            })
1497        ));
1498
1499        // Position is slightly off but within tolerance
1500        let found_nearby = layer.find_object_at(pos2(101.0, 101.0), &projection, None);
1501        assert!(matches!(
1502            found_nearby,
1503            Some(DraggedObject::PolygonNode {
1504                area_index: 0,
1505                node_index: 0
1506            })
1507        ));
1508
1509        // Position is too far
1510        let not_found = layer.find_object_at(pos2(200.0, 200.0), &projection, None);
1511        assert!(not_found.is_none());
1512    }
1513
1514    #[test]
1515    fn area_layer_serde() {
1516        let mut layer = AreaLayer::default();
1517        layer.add_area(Area {
1518            shape: AreaShape::Polygon(vec![(0.0, 0.0).into()]),
1519            stroke: Stroke::new(1.0, Color32::RED),
1520            fill: Color32::BLUE,
1521            fill_type: Default::default(),
1522        });
1523
1524        let json = serde_json::to_string(&layer).unwrap();
1525        let deserialized: AreaLayer = serde_json::from_str(&json).unwrap();
1526
1527        assert_eq!(deserialized.areas.len(), 1);
1528        assert_eq!(deserialized.mode, AreaMode::Disabled); // Restored to default
1529    }
1530
1531    #[test]
1532    fn test_can_triangulate_valid() {
1533        let projection = dummy_projection();
1534        let area = Area {
1535            shape: AreaShape::Polygon(vec![
1536                (0.0, 0.0).into(),
1537                (10.0, 0.0).into(),
1538                (0.0, 10.0).into(),
1539            ]),
1540            stroke: Default::default(),
1541            fill: Default::default(),
1542            fill_type: Default::default(),
1543        };
1544
1545        assert!(area.can_triangulate(&projection));
1546    }
1547
1548    #[test]
1549    fn test_can_triangulate_insufficient_points() {
1550        let projection = dummy_projection();
1551        let area = Area {
1552            shape: AreaShape::Polygon(vec![(0.0, 0.0).into(), (10.0, 0.0).into()]),
1553            stroke: Default::default(),
1554            fill: Default::default(),
1555            fill_type: Default::default(),
1556        };
1557
1558        // Should return true as we don't consider < 3 points as a triangulation failure
1559        // (it simply doesn't draw anything)
1560        assert!(area.can_triangulate(&projection));
1561    }
1562
1563    #[cfg(feature = "geojson")]
1564    mod geojson_tests {
1565        use super::*;
1566
1567        #[test]
1568        fn area_layer_geojson_polygon() {
1569            let mut layer = AreaLayer::default();
1570            layer.add_area(Area {
1571                shape: AreaShape::Polygon(vec![
1572                    (10.0, 20.0).into(),
1573                    (30.0, 40.0).into(),
1574                    (50.0, 60.0).into(),
1575                ]),
1576                stroke: Stroke::new(2.0, Color32::from_rgb(0, 0, 255)),
1577                fill: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
1578                fill_type: Default::default(),
1579            });
1580
1581            let geojson_str = layer.to_geojson_str().unwrap();
1582
1583            let mut new_layer = AreaLayer::default();
1584            new_layer.from_geojson_str(&geojson_str).unwrap();
1585
1586            assert_eq!(new_layer.areas.len(), 1);
1587            assert_eq!(layer.areas[0], new_layer.areas[0]);
1588        }
1589
1590        #[test]
1591        fn area_layer_geojson_circle() {
1592            let mut layer = AreaLayer::default();
1593            layer.add_area(Area {
1594                shape: AreaShape::Circle {
1595                    center: (10.0, 20.0).into(),
1596                    radius: 1000.0,
1597                    points: Some(32),
1598                },
1599                stroke: Default::default(),
1600                fill: Default::default(),
1601                fill_type: Default::default(),
1602            });
1603
1604            let geojson_str = layer.to_geojson_str().unwrap();
1605            let mut new_layer = AreaLayer::default();
1606            new_layer.from_geojson_str(&geojson_str).unwrap();
1607
1608            assert_eq!(new_layer.areas.len(), 1);
1609            assert_eq!(layer.areas[0].shape, new_layer.areas[0].shape);
1610        }
1611
1612        #[test]
1613        fn area_layer_geojson_ellipse() {
1614            let mut layer = AreaLayer::default();
1615            layer.add_area(Area {
1616                shape: AreaShape::Ellipse {
1617                    center: (10.0, 20.0).into(),
1618                    radius_major: 2000.0,
1619                    radius_minor: 1000.0,
1620                    rotation: 0.78,
1621                    points: Some(32),
1622                },
1623                stroke: Default::default(),
1624                fill: Default::default(),
1625                fill_type: Default::default(),
1626            });
1627
1628            let geojson_str = layer.to_geojson_str().unwrap();
1629            let mut new_layer = AreaLayer::default();
1630            new_layer.from_geojson_str(&geojson_str).unwrap();
1631
1632            assert_eq!(new_layer.areas.len(), 1);
1633            assert_eq!(layer.areas[0].shape, new_layer.areas[0].shape);
1634        }
1635    }
1636
1637    #[test]
1638    fn ellipse_get_points_with_fixed_number() {
1639        let projection = dummy_projection();
1640        let area = Area {
1641            shape: AreaShape::Ellipse {
1642                center: (0.0, 0.0).into(),
1643                radius_major: 2000.0,
1644                radius_minor: 1000.0,
1645                rotation: 0.5,
1646                points: Some(24),
1647            },
1648            stroke: Default::default(),
1649            fill: Default::default(),
1650            fill_type: Default::default(),
1651        };
1652
1653        let points = area.get_points(&projection);
1654        assert_eq!(points.len(), 24);
1655    }
1656
1657    #[test]
1658    fn ellipse_containment() {
1659        let projection = dummy_projection();
1660        let area = Area {
1661            shape: AreaShape::Ellipse {
1662                center: (0.0, 0.0).into(),
1663                radius_major: 2000.0,
1664                radius_minor: 1000.0,
1665                rotation: 0.0,
1666                points: None,
1667            },
1668            stroke: Default::default(),
1669            fill: Default::default(),
1670            fill_type: Default::default(),
1671        };
1672
1673        // Center is inside
1674        assert!(area.contains(projection.project((0.0, 0.0).into()), &projection));
1675
1676        // Slightly to the East (lon changes) should be inside
1677        let point_inside = GeoPos {
1678            lon: 0.005,
1679            lat: 0.0,
1680        };
1681        assert!(area.contains(projection.project(point_inside), &projection));
1682
1683        // Very far to the East (lon changes) should be outside
1684        let point_outside = GeoPos { lon: 0.5, lat: 0.0 };
1685        assert!(!area.contains(projection.project(point_outside), &projection));
1686    }
1687
1688    #[test]
1689    fn find_node_at_on_segment() {
1690        let projection = dummy_projection();
1691        let mut layer = AreaLayer::default();
1692
1693        let p1 = projection.unproject(pos2(100.0, 100.0));
1694        let p2 = projection.unproject(pos2(200.0, 100.0));
1695
1696        layer.add_area(Area {
1697            shape: AreaShape::Polygon(vec![p1, p2, projection.unproject(pos2(150.0, 200.0))]), // Triangle
1698            stroke: Default::default(),
1699            fill: Default::default(),
1700            fill_type: Default::default(),
1701        });
1702
1703        // Click exactly between p1 and p2
1704        let click_pos = pos2(150.0, 100.0);
1705
1706        // Should NOT find a node
1707        assert!(layer.find_node_at(click_pos, &projection, None).is_none());
1708
1709        // Should find the segment
1710        let segment = layer.find_line_segment_at(click_pos, &projection, None);
1711        assert!(segment.is_some());
1712        assert_eq!(segment.unwrap().0, 0); // area_index
1713        assert_eq!(segment.unwrap().1, 0);
1714    }
1715}