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