Skip to main content

egui_map_view/layers/
mod.rs

1//! Layers for the map view that can handle input, and draw on top of the map view different kinds of data.
2//!
3use egui::{Painter, Pos2, Response};
4use std::any::Any;
5
6use crate::projection::MapProjection;
7
8/// GeoJSON serialization and deserialization for layers.
9#[cfg(feature = "geojson")]
10pub mod geojson;
11
12/// Drawing layer
13#[cfg(feature = "drawing-layer")]
14pub mod drawing;
15
16/// Text layer
17#[cfg(feature = "text-layer")]
18pub mod text;
19
20/// Area layer
21#[cfg(feature = "area-layer")]
22pub mod area;
23
24// Tile layer
25#[cfg(feature = "tile-layer")]
26pub mod tile;
27
28/// A module for serializing and deserializing `Color32` to and from hex strings.
29pub(crate) mod serde_color32 {
30    use egui::Color32;
31    use serde::{self, Deserialize, Deserializer, Serializer};
32
33    /// Serializes a `Color32` to a hex string.
34    pub fn serialize<S>(color: &Color32, serializer: S) -> Result<S::Ok, S::Error>
35    where
36        S: Serializer,
37    {
38        serializer.serialize_str(&color.to_hex())
39    }
40
41    /// Deserializes a `Color32` from a hex string.
42    pub fn deserialize<'de, D>(deserializer: D) -> Result<Color32, D::Error>
43    where
44        D: Deserializer<'de>,
45    {
46        let s = <String as Deserialize>::deserialize(deserializer)?;
47        Color32::from_hex(&s).map_err(|err| serde::de::Error::custom(format!("{:?}", err)))
48    }
49}
50
51/// A module for serializing and deserializing `egui::Stroke`.
52pub(crate) mod serde_stroke {
53    use crate::layers::serde_color32;
54    use egui::{Color32, Stroke};
55    use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
56
57    #[derive(Serialize, Deserialize)]
58    struct StrokeHelper {
59        width: f32,
60        #[serde(with = "serde_color32")]
61        color: Color32,
62    }
63
64    pub fn serialize<S>(stroke: &Stroke, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: Serializer,
67    {
68        let helper = StrokeHelper {
69            width: stroke.width,
70            color: stroke.color,
71        };
72        helper.serialize(serializer)
73    }
74
75    pub fn deserialize<'de, D>(deserializer: D) -> Result<Stroke, D::Error>
76    where
77        D: Deserializer<'de>,
78    {
79        let helper = StrokeHelper::deserialize(deserializer)?;
80        Ok(Stroke {
81            width: helper.width,
82            color: helper.color,
83        })
84    }
85}
86
87/// A trait for map layers.
88pub trait Layer: Any {
89    /// Handles user input for the layer. Returns `true` if the input was handled and should not be
90    /// processed further by the map.
91    fn handle_input(&mut self, response: &Response, projection: &MapProjection) -> bool;
92
93    /// Draws the layer.
94    fn draw(&self, painter: &Painter, projection: &MapProjection);
95
96    /// Gets the layer as a `dyn Any`.
97    fn as_any(&self) -> &dyn Any;
98
99    /// Gets the layer as a mutable `dyn Any`.
100    fn as_any_mut(&mut self) -> &mut dyn Any;
101}
102
103/// Calculates the squared distance from a point to a line segment.
104pub(crate) fn dist_sq_to_segment(p: Pos2, a: Pos2, b: Pos2) -> f32 {
105    let ab = b - a;
106    let ap = p - a;
107    let l2 = ab.length_sq();
108
109    if l2 == 0.0 {
110        // The segment is a point.
111        return ap.length_sq();
112    }
113
114    // Project point p onto the line defined by a and b.
115    // `t` is the normalized distance from a to the projection.
116    let t = (ap.dot(ab) / l2).clamp(0.0, 1.0);
117
118    // The closest point on the line segment.
119    let closest_point = a + t * ab;
120
121    p.distance_sq(closest_point)
122}
123
124/// Calculates the projection factor of a point onto a line segment.
125/// Returns a value `t` from 0.0 to 1.0.
126pub(crate) fn projection_factor(p: Pos2, a: Pos2, b: Pos2) -> f32 {
127    let ab = b - a;
128    let ap = p - a;
129    let l2 = ab.length_sq();
130
131    if l2 == 0.0 {
132        return 0.0;
133    }
134
135    // Project point p onto the line defined by a and b.
136    (ap.dot(ab) / l2).clamp(0.0, 1.0)
137}
138
139/// Checks if two line segments intersect.
140pub(crate) fn segments_intersect(p1: Pos2, q1: Pos2, p2: Pos2, q2: Pos2) -> bool {
141    fn orientation(p: Pos2, q: Pos2, r: Pos2) -> i8 {
142        let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
143        if val.abs() < 1e-6 {
144            0 // Collinear
145        } else if val > 0.0 {
146            1 // Clockwise
147        } else {
148            -1 // Counter-clockwise
149        }
150    }
151
152    let o1 = orientation(p1, q1, p2);
153    let o2 = orientation(p1, q1, q2);
154    let o3 = orientation(p2, q2, p1);
155    let o4 = orientation(p2, q2, q1);
156
157    // General case: segments cross each other.
158    if o1 != o2 && o3 != o4 {
159        return true;
160    }
161
162    // Special cases for collinear points are ignored for simplicity,
163    // as they are less critical for this UI interaction.
164    false
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use egui::pos2;
171
172    const EPSILON: f32 = 1e-6;
173
174    #[test]
175    fn test_dist_sq_to_segment() {
176        let a = pos2(0.0, 0.0);
177        let b = pos2(10.0, 0.0);
178
179        // Point on the segment
180        let p1 = pos2(5.0, 0.0);
181        assert!((dist_sq_to_segment(p1, a, b) - 0.0).abs() < EPSILON);
182
183        // Point off the segment, projection is on the segment
184        let p2 = pos2(5.0, 5.0);
185        assert!((dist_sq_to_segment(p2, a, b) - 25.0).abs() < EPSILON); // 5*5
186
187        // Point off the segment, projection is before 'a'
188        let p3 = pos2(-5.0, 5.0);
189        assert!((dist_sq_to_segment(p3, a, b) - 50.0).abs() < EPSILON); // dist^2 from (-5,5) to (0,0) is 25+25 = 50
190
191        // Point off the segment, projection is after 'b'
192        let p4 = pos2(15.0, 5.0);
193        assert!((dist_sq_to_segment(p4, a, b) - 50.0).abs() < EPSILON); // dist^2 from (15,5) to (10,0) is 25+25 = 50
194
195        // Zero-length segment
196        let c = pos2(5.0, 5.0);
197        let p5 = pos2(10.0, 10.0);
198        assert!((dist_sq_to_segment(p5, c, c) - 50.0).abs() < EPSILON); // dist^2 from (10,10) to (5,5) is 25+25 = 50
199    }
200
201    #[test]
202    fn test_projection_factor() {
203        let a = pos2(0.0, 0.0);
204        let b = pos2(10.0, 0.0);
205
206        // Point is 'a'
207        assert!((projection_factor(a, a, b) - 0.0).abs() < EPSILON);
208
209        // Point is 'b'
210        assert!((projection_factor(b, a, b) - 1.0).abs() < EPSILON);
211
212        // Point is midpoint
213        let p1 = pos2(5.0, 0.0);
214        assert!((projection_factor(p1, a, b) - 0.5).abs() < EPSILON);
215
216        // Point projects to midpoint
217        let p2 = pos2(5.0, 5.0);
218        assert!((projection_factor(p2, a, b) - 0.5).abs() < EPSILON);
219
220        // Point projects before 'a' (clamped)
221        let p3 = pos2(-5.0, 5.0);
222        assert!((projection_factor(p3, a, b) - 0.0).abs() < EPSILON);
223
224        // Point projects after 'b' (clamped)
225        let p4 = pos2(15.0, 5.0);
226        assert!((projection_factor(p4, a, b) - 1.0).abs() < EPSILON);
227
228        // Zero-length segment
229        let c = pos2(5.0, 5.0);
230        let p5 = pos2(10.0, 10.0);
231        assert!((projection_factor(p5, c, c) - 0.0).abs() < EPSILON);
232    }
233
234    #[test]
235    fn test_segments_intersect() {
236        // General intersection
237        let p1 = pos2(0.0, 0.0);
238        let q1 = pos2(10.0, 10.0);
239        let p2 = pos2(0.0, 10.0);
240        let q2 = pos2(10.0, 0.0);
241        assert!(segments_intersect(p1, q1, p2, q2), "General intersection");
242
243        // No intersection, parallel
244        let p1 = pos2(0.0, 0.0);
245        let q1 = pos2(10.0, 0.0);
246        let p2 = pos2(0.0, 5.0);
247        let q2 = pos2(10.0, 5.0);
248        assert!(
249            !segments_intersect(p1, q1, p2, q2),
250            "Parallel, no intersection"
251        );
252
253        // No intersection, not parallel
254        let p1 = pos2(0.0, 0.0);
255        let q1 = pos2(1.0, 1.0);
256        let p2 = pos2(2.0, 2.0);
257        let q2 = pos2(3.0, 0.0);
258        assert!(
259            !segments_intersect(p1, q1, p2, q2),
260            "Not parallel, no intersection"
261        );
262
263        // T-junction
264        let p1 = pos2(0.0, 5.0);
265        let q1 = pos2(10.0, 5.0);
266        let p2 = pos2(5.0, 0.0);
267        let q2 = pos2(5.0, 5.0);
268        assert!(segments_intersect(p1, q1, p2, q2), "T-junction");
269
270        // Segments meeting at an endpoint
271        let p1 = pos2(0.0, 0.0);
272        let q1 = pos2(5.0, 5.0);
273        let p2 = pos2(5.0, 5.0);
274        let q2 = pos2(10.0, 0.0);
275        assert!(segments_intersect(p1, q1, p2, q2), "Meeting at an endpoint");
276
277        // Collinear, overlapping
278        let p1 = pos2(0.0, 0.0);
279        let q1 = pos2(10.0, 0.0);
280        let p2 = pos2(5.0, 0.0);
281        let q2 = pos2(15.0, 0.0);
282        assert!(
283            !segments_intersect(p1, q1, p2, q2),
284            "Collinear, overlapping"
285        );
286
287        // Collinear, non-overlapping
288        let p1 = pos2(0.0, 0.0);
289        let q1 = pos2(10.0, 0.0);
290        let p2 = pos2(11.0, 0.0);
291        let q2 = pos2(20.0, 0.0);
292        assert!(
293            !segments_intersect(p1, q1, p2, q2),
294            "Collinear, non-overlapping"
295        );
296
297        // Collinear, one contains another
298        let p1 = pos2(0.0, 0.0);
299        let q1 = pos2(10.0, 0.0);
300        let p2 = pos2(2.0, 0.0);
301        let q2 = pos2(8.0, 0.0);
302        assert!(!segments_intersect(p1, q1, p2, q2), "Collinear, contained");
303
304        // One segment is a point on the other segment
305        let p1 = pos2(0.0, 0.0);
306        let q1 = pos2(10.0, 0.0);
307        let p2 = pos2(5.0, 0.0);
308        let q2 = pos2(5.0, 0.0);
309        assert!(!segments_intersect(p1, q1, p2, q2), "Point on segment");
310    }
311}