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