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, dist_sq_to_segment, projection_factor, segments_intersect, serde_color32, serde_stroke,
48};
49use crate::projection::{GeoPos, MapProjection};
50use egui::{Color32, Mesh, Painter, Pos2, Response, Shape, Stroke};
51use log::warn;
52use serde::{Deserialize, Serialize};
53use std::any::Any;
54
55/// The mode of the `AreaLayer`.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum AreaMode {
58    /// The layer is not interactive.
59    #[default]
60    Disabled,
61    /// The user can add/remove/move nodes.
62    Modify,
63}
64
65/// The shape of a polygon area on the map.
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67pub enum AreaShape {
68    /// A freeform polygon defined by a list of points.
69    Polygon(Vec<GeoPos>),
70    /// A circle defined by its center and radius in meters.
71    Circle {
72        /// The geographical center of the circle.
73        center: GeoPos,
74        /// The radius of the circle in meters.
75        radius: f64,
76        /// 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.
77        points: Option<i64>,
78    },
79}
80
81/// How the interior of an area is filled.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
83pub enum FillType {
84    /// No fill — only the outline is drawn.
85    None,
86    /// Solid color fill.
87    #[default]
88    Solid,
89    /// Diagonal hatching lines using the stroke style.
90    Hatching,
91}
92
93/// A polygon area on the map.
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95pub struct Area {
96    /// The shape of the area.
97    pub shape: AreaShape,
98
99    /// The stroke style for drawing the polygon outlines.
100    #[serde(with = "serde_stroke")]
101    pub stroke: Stroke,
102
103    /// The fill color of the polygon.
104    #[serde(with = "serde_color32")]
105    pub fill: Color32,
106
107    /// How the interior of the area is filled.
108    #[serde(default)]
109    pub fill_type: FillType,
110}
111
112/// Represents what part of an area is being dragged.
113#[derive(Clone, Debug)]
114enum DraggedObject {
115    PolygonNode {
116        area_index: usize,
117        node_index: usize,
118    },
119    CircleCenter {
120        area_index: usize,
121    },
122    CircleRadius {
123        area_index: usize,
124    },
125}
126
127/// Layer implementation that allows the user to draw polygons on the map.
128#[derive(Clone, Serialize, Deserialize)]
129#[serde(default)]
130pub struct AreaLayer {
131    areas: Vec<Area>,
132
133    #[serde(skip)]
134    /// The radius of the nodes.
135    pub node_radius: f32,
136
137    #[serde(skip)]
138    /// The fill color of the nodes.
139    pub node_fill: Color32,
140
141    #[serde(skip)]
142    /// The current drawing mode.
143    pub mode: AreaMode,
144
145    #[serde(skip)]
146    dragged_object: Option<DraggedObject>,
147}
148
149impl Default for AreaLayer {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155impl AreaLayer {
156    /// Creates a new `AreaLayer`.
157    #[must_use]
158    pub fn new() -> Self {
159        Self {
160            areas: Vec::new(),
161            node_radius: 5.0,
162            node_fill: Color32::from_rgb(0, 128, 0),
163            mode: AreaMode::default(),
164            dragged_object: None,
165        }
166    }
167
168    /// Adds a new area to the layer.
169    pub fn add_area(&mut self, area: Area) {
170        self.areas.push(area);
171    }
172
173    /// Returns a reference to the areas in the layer.
174    #[must_use]
175    pub fn areas(&self) -> &Vec<Area> {
176        &self.areas
177    }
178
179    /// Returns a mutable reference to the areas in the layer.
180    pub fn areas_mut(&mut self) -> &mut Vec<Area> {
181        &mut self.areas
182    }
183
184    /// Serializes the layer to a `GeoJSON` `FeatureCollection`.
185    #[cfg(feature = "geojson")]
186    pub fn to_geojson_str(&self) -> Result<String, serde_json::Error> {
187        let features: Vec<geojson::Feature> = self
188            .areas
189            .clone()
190            .into_iter()
191            .map(geojson::Feature::from)
192            .collect();
193        let feature_collection = geojson::FeatureCollection {
194            bbox: None,
195            features,
196            foreign_members: None,
197        };
198        serde_json::to_string(&feature_collection)
199    }
200
201    /// Deserializes a `GeoJSON` `FeatureCollection` and adds the features to the layer.
202    #[cfg(feature = "geojson")]
203    pub fn from_geojson_str(&mut self, s: &str) -> Result<(), serde_json::Error> {
204        let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
205        let new_areas: Vec<Area> = feature_collection
206            .features
207            .into_iter()
208            .filter_map(|f| Area::try_from(f).ok())
209            .collect();
210        self.areas.extend(new_areas);
211        Ok(())
212    }
213
214    fn handle_modify_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
215        if response.double_clicked()
216            && let Some(pointer_pos) = response.interact_pointer_pos()
217        {
218            // TODO: This only works for polygons.
219            if self.find_node_at(pointer_pos, projection).is_none()
220                && let Some((area_idx, node_idx)) =
221                    self.find_line_segment_at(pointer_pos, projection)
222                && let Some(area) = self.areas.get_mut(area_idx)
223                && let AreaShape::Polygon(points) = &mut area.shape
224            {
225                let p1_screen = projection.project(points[node_idx]);
226                let p2_screen = projection.project(points[(node_idx + 1) % points.len()]);
227
228                let t = projection_factor(pointer_pos, p1_screen, p2_screen);
229
230                // Interpolate in screen space and unproject to get the new geographical position.
231                let new_pos_screen = p1_screen.lerp(p2_screen, t);
232                let new_pos_geo = projection.unproject(new_pos_screen);
233
234                points.insert(node_idx + 1, new_pos_geo);
235
236                // This interaction is fully handled, so we can return.
237                return response.hovered();
238            }
239        }
240
241        if response.drag_started()
242            && let Some(pointer_pos) = response.interact_pointer_pos()
243        {
244            self.dragged_object = self.find_object_at(pointer_pos, projection);
245        }
246
247        if response.dragged()
248            && let Some(dragged_object) = self.dragged_object.clone()
249            && let Some(pointer_pos) = response.interact_pointer_pos()
250        {
251            match dragged_object {
252                DraggedObject::PolygonNode {
253                    area_index,
254                    node_index,
255                } => {
256                    if self.is_move_valid(area_index, node_index, pointer_pos, projection)
257                        && let Some(area) = self.areas.get_mut(area_index)
258                    {
259                        let mut revert_info = None;
260                        if let AreaShape::Polygon(points) = &mut area.shape
261                            && let Some(node) = points.get_mut(node_index)
262                        {
263                            let old_pos = *node;
264                            *node = projection.unproject(pointer_pos);
265                            revert_info = Some(old_pos);
266                        }
267
268                        if let Some(old_pos) = revert_info
269                            && !area.can_triangulate(projection)
270                        {
271                            warn!("Triangulation failed, cancelling drag");
272                            self.dragged_object = None;
273                            if let AreaShape::Polygon(points) = &mut area.shape {
274                                points[node_index] = old_pos;
275                            }
276                        }
277                    }
278                }
279                DraggedObject::CircleCenter { area_index } => {
280                    if let Some(area) = self.areas.get_mut(area_index) {
281                        let mut revert_center = None;
282                        if let AreaShape::Circle { center, .. } = &mut area.shape {
283                            revert_center = Some(*center);
284                            *center = projection.unproject(pointer_pos);
285                        }
286
287                        if let Some(old_center) = revert_center
288                            && !area.can_triangulate(projection)
289                        {
290                            warn!("Triangulation failed, cancelling drag");
291                            self.dragged_object = None;
292                            if let AreaShape::Circle { center, .. } = &mut area.shape {
293                                *center = old_center;
294                            }
295                        }
296                    }
297                }
298                DraggedObject::CircleRadius { area_index } => {
299                    if let Some(area) = self.areas.get_mut(area_index) {
300                        let mut revert_radius = None;
301                        if let AreaShape::Circle {
302                            center,
303                            radius,
304                            points: _,
305                        } = &mut area.shape
306                        {
307                            revert_radius = Some(*radius);
308                            // Convert the new screen-space radius back to meters.
309                            let center_screen = projection.project(*center);
310                            let new_radius_pixels = pointer_pos.distance(center_screen);
311                            let new_edge_screen =
312                                center_screen + egui::vec2(new_radius_pixels, 0.0);
313                            let new_edge_geo = projection.unproject(new_edge_screen);
314
315                            // Calculate distance in meters. This is an approximation that works well for smaller distances.
316                            let distance_lon = (new_edge_geo.lon - center.lon).abs()
317                                * (111_320.0 * center.lat.to_radians().cos());
318                            let distance_lat = (new_edge_geo.lat - center.lat).abs() * 110_574.0;
319                            *radius = (distance_lon.powi(2) + distance_lat.powi(2)).sqrt();
320                        }
321
322                        if let Some(old_radius) = revert_radius
323                            && !area.can_triangulate(projection)
324                        {
325                            warn!("Triangulation failed, cancelling drag");
326                            self.dragged_object = None;
327                            if let AreaShape::Circle { radius, .. } = &mut area.shape {
328                                *radius = old_radius;
329                            }
330                        }
331                    }
332                }
333            }
334        }
335
336        if response.drag_stopped() {
337            self.dragged_object = None;
338        }
339
340        let is_dragging = self.dragged_object.is_some();
341
342        if is_dragging {
343            response.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
344        } else if let Some(pointer_pos) = response.hover_pos()
345            && self.find_object_at(pointer_pos, projection).is_some()
346        {
347            response.ctx.set_cursor_icon(egui::CursorIcon::Grab);
348        }
349
350        is_dragging
351            || (response.hovered()
352                && self
353                    .find_object_at(response.hover_pos().unwrap_or_default(), projection)
354                    .is_some())
355    }
356
357    fn find_object_at(
358        &self,
359        screen_pos: Pos2,
360        projection: &MapProjection,
361    ) -> Option<DraggedObject> {
362        let click_tolerance_sq = (self.node_radius * 3.0).powi(2);
363
364        for (area_idx, area) in self.areas.iter().enumerate().rev() {
365            match &area.shape {
366                AreaShape::Polygon(points) => {
367                    for (node_idx, node) in points.iter().enumerate() {
368                        let node_screen_pos = projection.project(*node);
369                        if node_screen_pos.distance_sq(screen_pos) < click_tolerance_sq {
370                            return Some(DraggedObject::PolygonNode {
371                                area_index: area_idx,
372                                node_index: node_idx,
373                            });
374                        }
375                    }
376                }
377                AreaShape::Circle {
378                    center,
379                    radius,
380                    points: _,
381                } => {
382                    let center_screen = projection.project(*center);
383
384                    // Convert radius from meters to screen pixels to correctly detect handle clicks.
385                    let point_on_circle_geo = GeoPos {
386                        lon: center.lon + (radius / (111_320.0 * center.lat.to_radians().cos())),
387                        lat: center.lat,
388                    };
389                    let point_on_circle_screen = projection.project(point_on_circle_geo);
390                    let radius_pixels = center_screen.distance(point_on_circle_screen);
391
392                    // Check for radius handle
393                    let distance_to_edge =
394                        (center_screen.distance(screen_pos) - radius_pixels).abs();
395                    if distance_to_edge < self.node_radius * 2.0 {
396                        return Some(DraggedObject::CircleRadius {
397                            area_index: area_idx,
398                        });
399                    }
400
401                    // Check for center
402                    if center_screen.distance_sq(screen_pos) < click_tolerance_sq {
403                        return Some(DraggedObject::CircleCenter {
404                            area_index: area_idx,
405                        });
406                    }
407                }
408            }
409        }
410
411        None
412    }
413
414    fn find_node_at(&self, screen_pos: Pos2, projection: &MapProjection) -> Option<(usize, usize)> {
415        match self.find_object_at(screen_pos, projection) {
416            Some(DraggedObject::PolygonNode {
417                area_index,
418                node_index,
419            }) => Some((area_index, node_index)),
420            _ => None,
421        }
422    }
423
424    fn find_line_segment_at(
425        &self,
426        screen_pos: Pos2,
427        projection: &MapProjection,
428    ) -> Option<(usize, usize)> {
429        let click_tolerance = (self.node_radius * 2.0).powi(2);
430
431        for (area_idx, area) in self.areas.iter().enumerate().rev() {
432            if let AreaShape::Polygon(points) = &area.shape {
433                if points.len() < 2 {
434                    continue;
435                }
436                for i in 0..points.len() {
437                    let p1 = projection.project(points[i]);
438                    let p2 = projection.project(points[(i + 1) % points.len()]);
439
440                    if dist_sq_to_segment(screen_pos, p1, p2) < click_tolerance {
441                        return Some((area_idx, i));
442                    }
443                }
444            }
445        }
446        None
447    }
448
449    /// Checks if moving a node to a new position would cause the polygon to self-intersect.
450    fn is_move_valid(
451        &self,
452        area_idx: usize,
453        node_idx: usize,
454        new_screen_pos: Pos2,
455        projection: &MapProjection,
456    ) -> bool {
457        let area = if let Some(area) = self.areas.get(area_idx) {
458            area
459        } else {
460            return false; // TODO: Should not happen
461        };
462
463        let points = match &area.shape {
464            AreaShape::Polygon(points) => points,
465            _ => return true, // Not a polygon, no intersections possible.
466        };
467
468        if points.len() < 3 {
469            return true;
470        }
471        let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
472
473        let n = screen_points.len();
474        let prev_node_idx = (node_idx + n - 1) % n;
475        let next_node_idx = (node_idx + 1) % n;
476
477        // The two edges that are being modified by the drag.
478        let new_edge1 = (screen_points[prev_node_idx], new_screen_pos);
479        let new_edge2 = (new_screen_pos, screen_points[next_node_idx]);
480
481        for i in 0..n {
482            let p1_idx = i;
483            let p2_idx = (i + 1) % n;
484
485            // Don't check against the edges connected to the dragged node.
486            if p1_idx == node_idx || p2_idx == node_idx {
487                continue;
488            }
489
490            let edge_to_check = (screen_points[p1_idx], screen_points[p2_idx]);
491
492            // Check against the first new edge.
493            if p1_idx != prev_node_idx
494                && p2_idx != prev_node_idx
495                && segments_intersect(new_edge1.0, new_edge1.1, edge_to_check.0, edge_to_check.1)
496            {
497                return false;
498            }
499
500            // Check against the second new edge.
501            if p1_idx != next_node_idx
502                && p2_idx != next_node_idx
503                && segments_intersect(new_edge2.0, new_edge2.1, edge_to_check.0, edge_to_check.1)
504            {
505                return false;
506            }
507        }
508
509        true
510    }
511}
512
513impl Area {
514    /// Checks if the area can be successfully triangulated.
515    fn can_triangulate(&self, projection: &MapProjection) -> bool {
516        let points = self.get_points(projection);
517        let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
518
519        if screen_points.len() < 3 {
520            return true;
521        }
522
523        let flat_points: Vec<f64> = screen_points
524            .iter()
525            .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
526            .collect();
527        earcutr::earcut(&flat_points, &[], 2).is_ok()
528    }
529
530    /// Returns the points of the area. For a circle, it generates a polygon approximation.
531    fn get_points(&self, projection: &MapProjection) -> Vec<GeoPos> {
532        match &self.shape {
533            AreaShape::Polygon(points) => points.clone(),
534            AreaShape::Circle {
535                center,
536                radius,
537                points,
538            } => {
539                // Convert radius from meters to screen pixels.
540                let center_geo = *center;
541                let point_on_circle_geo = GeoPos {
542                    lon: center_geo.lon
543                        + (radius / (111_320.0 * center_geo.lat.to_radians().cos())),
544                    lat: center_geo.lat,
545                };
546                let center_screen = projection.project(center_geo);
547                let point_on_circle_screen = projection.project(point_on_circle_geo);
548                let radius_pixels = center_screen.distance(point_on_circle_screen);
549
550                let num_points = if let Some(points) = points {
551                    *points
552                } else {
553                    // Automatically determine the number of points based on the circle's radius
554                    // to ensure it looks smooth.
555                    (f64::from(radius_pixels) * 2.0 * std::f64::consts::PI / 10.0).ceil() as i64
556                };
557                let mut circle_points = Vec::with_capacity(num_points as usize);
558
559                for i in 0..num_points {
560                    let angle = (i as f64 / num_points as f64) * 2.0 * std::f64::consts::PI;
561                    let point_screen = center_screen
562                        + egui::vec2(
563                            radius_pixels * angle.cos() as f32,
564                            radius_pixels * angle.sin() as f32,
565                        );
566                    circle_points.push(projection.unproject(point_screen));
567                }
568                circle_points
569            }
570        }
571    }
572}
573
574/// Generates diagonal hatching line segments clipped to the given polygon.
575///
576/// `screen_points` are the polygon vertices in screen space (must be >= 3 points).
577/// `spacing` is the distance in pixels between parallel hatching lines.
578/// `angle` is the angle of the hatching lines in radians (0 = horizontal, PI/4 = 45° diagonal).
579///
580/// Returns a list of line segments `(start, end)` that lie inside the polygon.
581fn generate_hatching_lines(screen_points: &[Pos2], spacing: f32, angle: f32) -> Vec<(Pos2, Pos2)> {
582    if screen_points.len() < 3 || spacing <= 0.0 {
583        return Vec::new();
584    }
585
586    // Direction along the hatching lines and perpendicular to them.
587    let dir = egui::vec2(angle.cos(), angle.sin());
588    let perp = egui::vec2(-angle.sin(), angle.cos());
589
590    // Project all polygon points onto the perpendicular axis to find the sweep range.
591    let mut min_perp = f32::MAX;
592    let mut max_perp = f32::MIN;
593    for p in screen_points {
594        let d = p.to_vec2().dot(perp);
595        min_perp = min_perp.min(d);
596        max_perp = max_perp.max(d);
597    }
598
599    let n = screen_points.len();
600    let mut segments = Vec::new();
601
602    // Sweep parallel lines across the polygon.
603    let mut offset = min_perp + spacing;
604    while offset < max_perp {
605        // A point on the current sweep line: origin + offset along the perpendicular.
606        let line_origin = Pos2::ZERO + perp * offset;
607
608        // Find intersections of this sweep line with every polygon edge.
609        let mut t_values: Vec<f32> = Vec::new();
610        for i in 0..n {
611            let a = screen_points[i];
612            let b = screen_points[(i + 1) % n];
613            let edge = b - a;
614
615            // Solve: a + t_edge * edge = line_origin + t_line * dir
616            // Cross product form: (a - line_origin) × dir = t_edge * (edge × dir)
617            let denom = edge.x * dir.y - edge.y * dir.x;
618            if denom.abs() < 1e-9 {
619                continue; // Edge is parallel to the hatching line.
620            }
621
622            let diff = a - line_origin;
623            let t_edge = -(diff.x * dir.y - diff.y * dir.x) / denom;
624
625            if (0.0..=1.0).contains(&t_edge) {
626                // Compute t_line: the parameter along the sweep line direction.
627                let t_line = if dir.x.abs() > dir.y.abs() {
628                    (a.x - line_origin.x + t_edge * edge.x) / dir.x
629                } else {
630                    (a.y - line_origin.y + t_edge * edge.y) / dir.y
631                };
632                t_values.push(t_line);
633            }
634        }
635
636        // Sort intersections along the sweep line.
637        t_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
638
639        // Pair up intersections (even-odd rule) to get interior segments.
640        for pair in t_values.chunks_exact(2) {
641            let p1 = line_origin + dir * pair[0];
642            let p2 = line_origin + dir * pair[1];
643            segments.push((p1, p2));
644        }
645
646        offset += spacing;
647    }
648
649    segments
650}
651
652impl Layer for AreaLayer {
653    fn as_any(&self) -> &dyn Any {
654        self
655    }
656
657    fn as_any_mut(&mut self) -> &mut dyn Any {
658        self
659    }
660
661    fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
662        match self.mode {
663            AreaMode::Disabled => false,
664            AreaMode::Modify => self.handle_modify_input(response, projection),
665        }
666    }
667
668    fn draw(&self, painter: &Painter, projection: &MapProjection) {
669        for area in &self.areas {
670            let points = area.get_points(projection);
671            let screen_points: Vec<Pos2> = points.iter().map(|p| projection.project(*p)).collect();
672
673            // Draw polygon outline
674            if screen_points.len() >= 3 {
675                // Use a generic path for the stroke.
676                let path_shape = Shape::Path(egui::epaint::PathShape {
677                    points: screen_points.clone(),
678                    closed: true,
679                    fill: Color32::TRANSPARENT,
680                    stroke: area.stroke.into(),
681                });
682                painter.add(path_shape);
683
684                match area.fill_type {
685                    FillType::None => { /* No fill */ }
686                    FillType::Solid => {
687                        // Triangulate for the fill.
688                        let flat_points: Vec<f64> = screen_points
689                            .iter()
690                            .flat_map(|p| [f64::from(p.x), f64::from(p.y)])
691                            .collect();
692                        match earcutr::earcut(&flat_points, &[], 2) {
693                            Ok(indices) => {
694                                let mesh = Mesh {
695                                    vertices: screen_points
696                                        .iter()
697                                        .map(|p| egui::epaint::Vertex {
698                                            pos: *p,
699                                            uv: Default::default(),
700                                            color: area.fill,
701                                        })
702                                        .collect(),
703                                    indices: indices.into_iter().map(|i| i as u32).collect(),
704                                    ..Default::default()
705                                };
706                                painter.add(Shape::Mesh(mesh.into()));
707                            }
708                            Err(e) => {
709                                warn!("Failed to triangulate area: {e:?}");
710                            }
711                        }
712                    }
713                    FillType::Hatching => {
714                        let segments = generate_hatching_lines(
715                            &screen_points,
716                            8.0,
717                            std::f32::consts::FRAC_PI_4,
718                        );
719                        for (a, b) in segments {
720                            painter.line_segment([a, b], area.stroke);
721                        }
722                    }
723                }
724            } else {
725                warn!("Invalid amount of points in area. {area:?}");
726            }
727
728            // Draw nodes only when in modify mode
729            if self.mode == AreaMode::Modify {
730                match &area.shape {
731                    AreaShape::Polygon(_) => {
732                        for point in &screen_points {
733                            painter.circle_filled(*point, self.node_radius, self.node_fill);
734                        }
735                    }
736                    AreaShape::Circle {
737                        center,
738                        radius,
739                        points: _,
740                    } => {
741                        let center_screen = projection.project(*center);
742
743                        // Convert radius from meters to screen pixels to correctly position the handle.
744                        let point_on_circle_geo = GeoPos {
745                            lon: center.lon
746                                + (radius / (111_320.0 * center.lat.to_radians().cos())),
747                            lat: center.lat,
748                        };
749                        let point_on_circle_screen = projection.project(point_on_circle_geo);
750                        let radius_pixels = center_screen.distance(point_on_circle_screen);
751
752                        painter.circle_filled(center_screen, self.node_radius, self.node_fill);
753                        let radius_handle_pos = center_screen + egui::vec2(radius_pixels, 0.0);
754                        painter.circle_filled(radius_handle_pos, self.node_radius, self.node_fill);
755                    }
756                }
757            }
758        }
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::projection::MapProjection;
766    use egui::{Rect, pos2, vec2};
767
768    // Helper for creating a dummy projection for tests
769    fn dummy_projection() -> MapProjection {
770        MapProjection::new(
771            10,                // zoom
772            (0.0, 0.0).into(), // center
773            Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 1000.0)),
774        )
775    }
776
777    #[test]
778    fn area_layer_new() {
779        let layer = AreaLayer::default();
780        assert_eq!(layer.mode, AreaMode::Disabled);
781        assert!(layer.areas.is_empty());
782        assert_eq!(layer.node_radius, 5.0);
783    }
784
785    #[test]
786    fn area_layer_add_area() {
787        let mut layer = AreaLayer::default();
788        assert_eq!(layer.areas.len(), 0);
789
790        layer.add_area(Area {
791            shape: AreaShape::Polygon(vec![
792                (0.0, 0.0).into(),
793                (1.0, 0.0).into(),
794                (0.0, 1.0).into(),
795            ]),
796            stroke: Default::default(),
797            fill: Default::default(),
798            fill_type: Default::default(),
799        });
800
801        assert_eq!(layer.areas.len(), 1);
802    }
803
804    #[test]
805    fn circle_get_points_with_fixed_number() {
806        let projection = dummy_projection();
807        let area = Area {
808            shape: AreaShape::Circle {
809                center: (0.0, 0.0).into(),
810                radius: 1000.0,
811                points: Some(16),
812            },
813            stroke: Default::default(),
814            fill: Default::default(),
815            fill_type: Default::default(),
816        };
817
818        let points = area.get_points(&projection);
819        assert_eq!(points.len(), 16);
820    }
821
822    #[test]
823    fn find_object_at_empty() {
824        let layer = AreaLayer::default();
825        let projection = dummy_projection();
826        let position = pos2(100.0, 100.0);
827
828        assert!(layer.find_object_at(position, &projection).is_none());
829    }
830
831    #[test]
832    fn find_object_at_polygon_node() {
833        let projection = dummy_projection();
834        let mut layer = AreaLayer::default();
835        let geo_pos = projection.unproject(pos2(100.0, 100.0));
836
837        layer.add_area(Area {
838            shape: AreaShape::Polygon(vec![geo_pos]),
839            stroke: Default::default(),
840            fill: Default::default(),
841            fill_type: Default::default(),
842        });
843
844        // Position is exactly on the node
845        let found = layer.find_object_at(pos2(100.0, 100.0), &projection);
846        assert!(matches!(
847            found,
848            Some(DraggedObject::PolygonNode {
849                area_index: 0,
850                node_index: 0
851            })
852        ));
853
854        // Position is slightly off but within tolerance
855        let found_nearby = layer.find_object_at(pos2(101.0, 101.0), &projection);
856        assert!(matches!(
857            found_nearby,
858            Some(DraggedObject::PolygonNode {
859                area_index: 0,
860                node_index: 0
861            })
862        ));
863
864        // Position is too far
865        let not_found = layer.find_object_at(pos2(200.0, 200.0), &projection);
866        assert!(not_found.is_none());
867    }
868
869    #[test]
870    fn area_layer_serde() {
871        let mut layer = AreaLayer::default();
872        layer.add_area(Area {
873            shape: AreaShape::Polygon(vec![(0.0, 0.0).into()]),
874            stroke: Stroke::new(1.0, Color32::RED),
875            fill: Color32::BLUE,
876            fill_type: Default::default(),
877        });
878
879        let json = serde_json::to_string(&layer).unwrap();
880        let deserialized: AreaLayer = serde_json::from_str(&json).unwrap();
881
882        assert_eq!(deserialized.areas.len(), 1);
883        assert_eq!(deserialized.mode, AreaMode::Disabled); // Restored to default
884    }
885
886    #[test]
887    fn test_can_triangulate_valid() {
888        let projection = dummy_projection();
889        let area = Area {
890            shape: AreaShape::Polygon(vec![
891                (0.0, 0.0).into(),
892                (10.0, 0.0).into(),
893                (0.0, 10.0).into(),
894            ]),
895            stroke: Default::default(),
896            fill: Default::default(),
897            fill_type: Default::default(),
898        };
899
900        assert!(area.can_triangulate(&projection));
901    }
902
903    #[test]
904    fn test_can_triangulate_insufficient_points() {
905        let projection = dummy_projection();
906        let area = Area {
907            shape: AreaShape::Polygon(vec![(0.0, 0.0).into(), (10.0, 0.0).into()]),
908            stroke: Default::default(),
909            fill: Default::default(),
910            fill_type: Default::default(),
911        };
912
913        // Should return true as we don't consider < 3 points as a triangulation failure
914        // (it simply doesn't draw anything)
915        assert!(area.can_triangulate(&projection));
916    }
917
918    #[cfg(feature = "geojson")]
919    mod geojson_tests {
920        use super::*;
921
922        #[test]
923        fn area_layer_geojson_polygon() {
924            let mut layer = AreaLayer::default();
925            layer.add_area(Area {
926                shape: AreaShape::Polygon(vec![
927                    (10.0, 20.0).into(),
928                    (30.0, 40.0).into(),
929                    (50.0, 60.0).into(),
930                ]),
931                stroke: Stroke::new(2.0, Color32::from_rgb(0, 0, 255)),
932                fill: Color32::from_rgba_unmultiplied(255, 0, 0, 128),
933                fill_type: Default::default(),
934            });
935
936            let geojson_str = layer.to_geojson_str().unwrap();
937
938            let mut new_layer = AreaLayer::default();
939            new_layer.from_geojson_str(&geojson_str).unwrap();
940
941            assert_eq!(new_layer.areas.len(), 1);
942            assert_eq!(layer.areas[0], new_layer.areas[0]);
943        }
944
945        #[test]
946        fn area_layer_geojson_circle() {
947            let mut layer = AreaLayer::default();
948            layer.add_area(Area {
949                shape: AreaShape::Circle {
950                    center: (10.0, 20.0).into(),
951                    radius: 1000.0,
952                    points: Some(32),
953                },
954                stroke: Default::default(),
955                fill: Default::default(),
956                fill_type: Default::default(),
957            });
958
959            let geojson_str = layer.to_geojson_str().unwrap();
960            let mut new_layer = AreaLayer::default();
961            new_layer.from_geojson_str(&geojson_str).unwrap();
962
963            assert_eq!(new_layer.areas.len(), 1);
964            assert_eq!(layer.areas[0].shape, new_layer.areas[0].shape);
965        }
966    }
967
968    #[test]
969    fn find_node_at_on_segment() {
970        let projection = dummy_projection();
971        let mut layer = AreaLayer::default();
972
973        let p1 = projection.unproject(pos2(100.0, 100.0));
974        let p2 = projection.unproject(pos2(200.0, 100.0));
975
976        layer.add_area(Area {
977            shape: AreaShape::Polygon(vec![p1, p2, projection.unproject(pos2(150.0, 200.0))]), // Triangle
978            stroke: Default::default(),
979            fill: Default::default(),
980            fill_type: Default::default(),
981        });
982
983        // Click exactly between p1 and p2
984        let click_pos = pos2(150.0, 100.0);
985
986        // Should NOT find a node
987        assert!(layer.find_node_at(click_pos, &projection).is_none());
988
989        // Should find the segment
990        let segment = layer.find_line_segment_at(click_pos, &projection);
991        assert!(segment.is_some());
992        assert_eq!(segment.unwrap().0, 0); // area_index
993        assert_eq!(segment.unwrap().1, 0);
994    }
995}