Skip to main content

egui_map_view/layers/
drawing.rs

1//! A layer for freeform drawing on the map.
2//!
3//! # Example
4//!
5//! ```no_run
6//! use eframe::egui;
7//! use egui_map_view::{layers::drawing::DrawingLayer, layers::drawing::DrawMode, Map, config::OpenStreetMapConfig};
8//!
9//! struct MyApp {
10//!     map: Map,
11//! }
12//!
13//! impl Default for MyApp {
14//!   fn default() -> Self {
15//!     let mut map = Map::new(OpenStreetMapConfig::default());
16//!      map.add_layer("drawing", DrawingLayer::default());
17//!      if let Some(drawing_layer) = map.layer_mut::<DrawingLayer>("drawing") {
18//!        drawing_layer.draw_mode = DrawMode::Draw;
19//!      }
20//!      Self { map }
21//!    }
22//! }
23//!
24//! impl eframe::App for MyApp {
25//!     fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
26//!         egui::CentralPanel::default().show(ctx, |ui| {
27//!             ui.add(&mut self.map);
28//!         });
29//!     }
30//! }
31//! ```
32use crate::layers::{Layer, dist_sq_to_segment, projection_factor, serde_stroke};
33use crate::projection::{GeoPos, MapProjection};
34use egui::{Color32, Painter, Pos2, Response, Stroke};
35use serde::{Deserialize, Serialize};
36use std::any::Any;
37
38/// A polyline on the map.
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40pub struct Polyline(pub Vec<GeoPos>);
41
42/// The mode of the `DrawingLayer`.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub enum DrawMode {
45    /// The layer is not interactive.
46    #[default]
47    Disabled,
48    /// The user can draw on the map.
49    Draw,
50    /// The user can erase drawings.
51    Erase,
52}
53
54/// Layer implementation that allows the user to draw polylines on the map.
55#[derive(Clone, Serialize, Deserialize)]
56#[serde(default)]
57pub struct DrawingLayer {
58    polylines: Vec<Polyline>,
59
60    /// The stroke style for drawing aka line width and color.
61    #[serde(with = "serde_stroke")]
62    pub stroke: Stroke,
63
64    /// The current drawing mode.
65    #[serde(skip)]
66    pub draw_mode: DrawMode,
67}
68
69impl DrawingLayer {
70    /// Serializes the layer to a GeoJSON `FeatureCollection`.
71    #[cfg(feature = "geojson")]
72    /// Serializes the layer to a GeoJSON `FeatureCollection`.
73    #[cfg(feature = "geojson")]
74    pub fn to_geojson_str(&self, layer_id: &str) -> Result<String, serde_json::Error> {
75        let features: Vec<geojson::Feature> = self
76            .polylines
77            .clone()
78            .into_iter()
79            .map(|p| {
80                let mut feature = geojson::Feature::from(p);
81                if let Some(properties) = &mut feature.properties {
82                    properties.insert(
83                        "stroke_width".to_string(),
84                        serde_json::Value::from(self.stroke.width),
85                    );
86                    properties.insert(
87                        "stroke_color".to_string(),
88                        serde_json::Value::String(self.stroke.color.to_hex()),
89                    );
90                    properties.insert(
91                        "layer_id".to_string(),
92                        serde_json::Value::String(layer_id.to_string()),
93                    );
94                }
95                feature
96            })
97            .collect();
98
99        let mut foreign_members = serde_json::Map::new();
100        foreign_members.insert(
101            "stroke_width".to_string(),
102            serde_json::Value::from(self.stroke.width),
103        );
104        foreign_members.insert(
105            "stroke_color".to_string(),
106            serde_json::Value::String(self.stroke.color.to_hex()),
107        );
108        foreign_members.insert(
109            "layer_id".to_string(),
110            serde_json::Value::String(layer_id.to_string()),
111        );
112
113        let feature_collection = geojson::FeatureCollection {
114            bbox: None,
115            features,
116            foreign_members: Some(foreign_members),
117        };
118        serde_json::to_string(&feature_collection)
119    }
120
121    /// Deserializes a GeoJSON `FeatureCollection` and adds the features to the layer.
122    ///
123    /// If `layer_id` is provided, only features with a matching `layer_id` property will be added.
124    /// If `layer_id` is `None`, all valid features will be added.
125    #[cfg(feature = "geojson")]
126    pub fn from_geojson_str(
127        &mut self,
128        s: &str,
129        layer_id: Option<&str>,
130    ) -> Result<(), serde_json::Error> {
131        let feature_collection: geojson::FeatureCollection = serde_json::from_str(s)?;
132        let new_polylines: Vec<Polyline> = feature_collection
133            .features
134            .iter()
135            .filter_map(|f| {
136                // Filter by layer_id if provided
137                if let Some(target_id) = layer_id {
138                    if let Some(properties) = &f.properties {
139                        if let Some(val) = properties.get("layer_id") {
140                            if let Some(id) = val.as_str() {
141                                if id != target_id {
142                                    return None;
143                                }
144                            } else {
145                                // layer_id property exists but is not a string, treat as mismatch
146                                return None;
147                            }
148                        } else {
149                            // layer_id property missing, treat as mismatch
150                            return None;
151                        }
152                    } else {
153                        // No properties, treat as mismatch
154                        return None;
155                    }
156                }
157
158                let polyline = Polyline::try_from(f.clone()).ok();
159                if polyline.is_some() {
160                    if let Some(properties) = &f.properties {
161                        if let Some(value) = properties.get("stroke_width") {
162                            if let Some(width) = value.as_f64() {
163                                self.stroke.width = width as f32;
164                            }
165                        }
166                        if let Some(value) = properties.get("stroke_color") {
167                            if let Some(s) = value.as_str() {
168                                if let Ok(color) = Color32::from_hex(s) {
169                                    self.stroke.color = color;
170                                }
171                            }
172                        }
173                    }
174                }
175                polyline
176            })
177            .collect();
178        self.polylines.extend(new_polylines);
179
180        if let Some(foreign_members) = feature_collection.foreign_members {
181            // Check layer_id in foreign members if filtering is enabled?
182            // The requirement says "only those Polylines are deserialized into the the layer that have that layer ID in place".
183            // This implies filtering individual features.
184            // However, we might want to respect global properties if they match.
185            // But for now, let's just apply global properties if we processed any features OR if we are not filtering?
186            // Or maybe just always apply them if they exist?
187            // Let's stick to applying them always for now, as it was before, but maybe we should check layer_id here too?
188            // The prompt specifically talks about Polylines (features).
189            // "only those Polylines are deserialized into the the layer that have that layer ID in place"
190
191            if let Some(value) = foreign_members.get("stroke_width") {
192                if let Some(width) = value.as_f64() {
193                    self.stroke.width = width as f32;
194                }
195            }
196            if let Some(value) = foreign_members.get("stroke_color") {
197                if let Some(s) = value.as_str() {
198                    if let Ok(color) = Color32::from_hex(s) {
199                        self.stroke.color = color;
200                    }
201                }
202            }
203        }
204
205        Ok(())
206    }
207
208    /// Creates a new `DrawingLayer`.
209    pub fn new(stroke: Stroke) -> Self {
210        Self {
211            polylines: Vec::new(),
212            stroke,
213            draw_mode: DrawMode::default(),
214        }
215    }
216}
217
218impl Default for DrawingLayer {
219    fn default() -> Self {
220        Self {
221            polylines: Vec::new(),
222            stroke: Stroke::new(2.0, Color32::RED),
223            draw_mode: DrawMode::default(),
224        }
225    }
226}
227
228impl DrawingLayer {
229    fn handle_draw_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
230        if response.hovered() {
231            response.ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
232        }
233
234        if response.clicked() {
235            if let Some(pointer_pos) = response.interact_pointer_pos() {
236                let geo_pos = projection.unproject(pointer_pos);
237                if let Some(last_line) = self.polylines.last_mut()
238                    && response.ctx.input(|i| i.modifiers.shift)
239                {
240                    last_line.0.push(geo_pos);
241                } else {
242                    // No polylines exist yet, so create a new one.
243                    let geo_pos2 = projection.unproject(pointer_pos + egui::vec2(1.0, 0.0));
244                    self.polylines.push(Polyline(vec![geo_pos, geo_pos2]));
245                }
246            }
247        }
248
249        if response.drag_started() {
250            self.polylines.push(Polyline(Vec::new()));
251        }
252
253        if response.dragged() {
254            if let Some(pointer_pos) = response.interact_pointer_pos() {
255                if let Some(last_line) = self.polylines.last_mut() {
256                    let geo_pos = projection.unproject(pointer_pos);
257                    last_line.0.push(geo_pos);
258                }
259            }
260        }
261
262        // When drawing, we consume all interactions over the map,
263        // so that the map does not pan or zoom.
264        response.hovered()
265    }
266
267    fn handle_erase_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
268        if response.hovered() {
269            response.ctx.set_cursor_icon(egui::CursorIcon::NotAllowed);
270        }
271
272        if response.dragged() || response.clicked() {
273            if let Some(pointer_pos) = response.interact_pointer_pos() {
274                self.erase_at(pointer_pos, projection);
275            }
276        }
277        response.hovered()
278    }
279
280    fn erase_at(&mut self, pointer_pos: Pos2, projection: &MapProjection) {
281        let erase_radius_screen = self.stroke.width;
282        let erase_radius_sq = erase_radius_screen * erase_radius_screen;
283
284        let old_polylines = std::mem::take(&mut self.polylines);
285        self.polylines = old_polylines
286            .into_iter()
287            .flat_map(|polyline| {
288                split_polyline_by_erase_circle(
289                    &polyline.0,
290                    pointer_pos,
291                    erase_radius_sq,
292                    projection,
293                )
294                .into_iter()
295                .map(Polyline)
296            })
297            .collect();
298    }
299}
300
301impl Layer for DrawingLayer {
302    fn as_any(&self) -> &dyn Any {
303        self
304    }
305
306    fn as_any_mut(&mut self) -> &mut dyn Any {
307        self
308    }
309
310    fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool {
311        match self.draw_mode {
312            DrawMode::Disabled => false,
313            DrawMode::Draw => self.handle_draw_input(response, projection),
314            DrawMode::Erase => self.handle_erase_input(response, projection),
315        }
316    }
317
318    fn draw(&self, painter: &Painter, projection: &MapProjection) {
319        for polyline in &self.polylines {
320            if polyline.0.len() > 1 {
321                let screen_points: Vec<egui::Pos2> =
322                    polyline.0.iter().map(|p| projection.project(*p)).collect();
323                painter.add(egui::Shape::line(screen_points, self.stroke));
324            }
325        }
326    }
327}
328
329/// Splits a polyline into multiple polylines based on whether segments are within the erase radius.
330fn split_polyline_by_erase_circle(
331    polyline: &[GeoPos],
332    pointer_pos: Pos2,
333    erase_radius_sq: f32,
334    projection: &MapProjection,
335) -> Vec<Vec<GeoPos>> {
336    if polyline.len() < 2 {
337        return vec![];
338    }
339
340    let screen_points: Vec<Pos2> = polyline.iter().map(|p| projection.project(*p)).collect();
341
342    let mut new_polylines = Vec::new();
343    let mut current_line = Vec::new();
344    let mut in_visible_part = true;
345
346    // Check if the first segment is erased to correctly set initial state.
347    if dist_sq_to_segment(pointer_pos, screen_points[0], screen_points[1]) < erase_radius_sq {
348        in_visible_part = false;
349    } else {
350        current_line.push(polyline[0]);
351    }
352
353    for i in 0..(polyline.len() - 1) {
354        let p2_geo = polyline[i + 1];
355        let p1_screen = screen_points[i];
356        let p2_screen = screen_points[i + 1];
357
358        let segment_is_erased =
359            dist_sq_to_segment(pointer_pos, p1_screen, p2_screen) < erase_radius_sq;
360
361        if in_visible_part {
362            if segment_is_erased {
363                // Transition from visible to erased.
364                let t = projection_factor(pointer_pos, p1_screen, p2_screen);
365                let split_point_screen = p1_screen.lerp(p2_screen, t);
366                let split_point_geo = projection.unproject(split_point_screen);
367                current_line.push(split_point_geo);
368
369                if current_line.len() > 1 {
370                    new_polylines.push(std::mem::take(&mut current_line));
371                }
372                in_visible_part = false;
373            } else {
374                // Continue visible part.
375                current_line.push(p2_geo);
376            }
377        } else {
378            // In erased part
379            if !segment_is_erased {
380                // Transition from erased to visible.
381                let t = projection_factor(pointer_pos, p1_screen, p2_screen);
382                let split_point_screen = p1_screen.lerp(p2_screen, t);
383                let split_point_geo = projection.unproject(split_point_screen);
384
385                // Start new line.
386                current_line.push(split_point_geo);
387                current_line.push(p2_geo);
388                in_visible_part = true;
389            }
390            // Continue in erased part, do nothing.
391        }
392    }
393
394    if current_line.len() > 1 {
395        new_polylines.push(current_line);
396    }
397
398    new_polylines
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn drawing_layer_new() {
407        let layer = DrawingLayer::default();
408        assert_eq!(layer.draw_mode, DrawMode::Disabled);
409        assert!(layer.polylines.is_empty());
410    }
411
412    #[test]
413    fn drawing_layer_as_any() {
414        let layer = DrawingLayer::default();
415        assert!(layer.as_any().is::<DrawingLayer>());
416    }
417
418    #[test]
419    fn drawing_layer_as_any_mut() {
420        let mut layer = DrawingLayer::default();
421        assert!(layer.as_any_mut().is::<DrawingLayer>());
422    }
423
424    #[test]
425    fn drawing_layer_serde() {
426        let mut layer = DrawingLayer::default();
427        layer.draw_mode = DrawMode::Draw; // This should not be serialized.
428        layer.polylines.push(Polyline(vec![
429            GeoPos { lon: 1.0, lat: 2.0 },
430            GeoPos { lon: 3.0, lat: 4.0 },
431        ]));
432        layer.stroke = Stroke::new(5.0, Color32::BLUE); // This should not be serialized.
433
434        let json = serde_json::to_string(&layer).unwrap();
435
436        // The serialized string should only contain polylines.
437        assert!(json.contains(r##""polylines":[[{"lon":1.0,"lat":2.0},{"lon":3.0,"lat":4.0}]],"stroke":{"width":5.0,"color":"#0000ffff"}"##));
438        assert!(!json.contains("draw_mode"));
439
440        let deserialized: DrawingLayer = serde_json::from_str(&json).unwrap();
441
442        // Check that polylines are restored correctly.
443        assert_eq!(deserialized.polylines, layer.polylines);
444
445        // Check that the stroke information is correct
446        assert_eq!(deserialized.stroke.width, 5.0);
447        assert_eq!(deserialized.stroke.color, Color32::BLUE);
448
449        // Default is drawmode disabled and its not serializable
450        assert_eq!(deserialized.draw_mode, DrawMode::Disabled);
451    }
452
453    #[cfg(feature = "geojson")]
454    mod geojson_tests {
455        use super::*;
456
457        #[test]
458        fn drawing_layer_geojson() {
459            let mut layer = DrawingLayer::default();
460            layer.polylines.push(Polyline(vec![
461                (10.0, 20.0).into(),
462                (30.0, 40.0).into(),
463                (50.0, 60.0).into(),
464            ]));
465            layer.stroke = Stroke::new(5.0, Color32::BLUE);
466
467            let geojson_str = layer.to_geojson_str("my_layer").unwrap();
468
469            // Test deserialization with matching ID
470            let mut new_layer = DrawingLayer::default();
471            new_layer
472                .from_geojson_str(&geojson_str, Some("my_layer"))
473                .unwrap();
474
475            assert_eq!(new_layer.polylines.len(), 1);
476            assert_eq!(layer.polylines[0], new_layer.polylines[0]);
477            assert_eq!(layer.stroke, new_layer.stroke);
478
479            // Test deserialization with non-matching ID
480            let mut other_layer = DrawingLayer::default();
481            other_layer
482                .from_geojson_str(&geojson_str, Some("other_layer"))
483                .unwrap();
484            assert_eq!(other_layer.polylines.len(), 0);
485
486            // Test deserialization with None ID (should include all)
487            let mut all_layer = DrawingLayer::default();
488            all_layer.from_geojson_str(&geojson_str, None).unwrap();
489            assert_eq!(all_layer.polylines.len(), 1);
490        }
491    }
492}