Skip to main content

egui_map_view/layers/
geojson.rs

1//! GeoJSON serialization and deserialization for layers.
2
3use super::area::{Area, AreaShape, FillType};
4use super::drawing::Polyline;
5use super::text::{Text, TextSize};
6use crate::projection::GeoPos;
7use egui::{Color32, Stroke};
8use geojson::{Feature, Geometry, GeometryValue};
9use serde_json::{Map, Value as JsonValue};
10
11/// Adds crate name and version to the feature properties.
12fn add_version_to_properties(properties: &mut Map<String, JsonValue>) {
13    properties.insert(
14        "x-egui-map-view-crate-name".to_string(),
15        JsonValue::String(env!("CARGO_PKG_NAME").to_string()),
16    );
17    properties.insert(
18        "x-egui-map-view-crate-version".to_string(),
19        JsonValue::String(env!("CARGO_PKG_VERSION").to_string()),
20    );
21}
22
23/// Checks the crate version from the feature properties and logs a warning on mismatch.
24fn check_version_from_properties(properties: &Map<String, JsonValue>) {
25    if let (Some(name), Some(version)) = (
26        properties
27            .get("x-egui-map-view-crate-name")
28            .and_then(|v| v.as_str()),
29        properties
30            .get("x-egui-map-view-crate-version")
31            .and_then(|v| v.as_str()),
32    ) {
33        if name == env!("CARGO_PKG_NAME") && version != env!("CARGO_PKG_VERSION") {
34            log::warn!(
35                "GeoJSON feature was created with a different version of {}. File version: {}, current version: {}. This might lead to unexpected behavior.",
36                name,
37                version,
38                env!("CARGO_PKG_VERSION")
39            );
40        }
41    } else {
42        log::warn!("No egui-map-view version information found in feature properties.");
43    }
44}
45
46impl From<Area> for Feature {
47    fn from(area: Area) -> Self {
48        let mut feature = Feature::default();
49        let mut properties = Map::new();
50        add_version_to_properties(&mut properties);
51
52        properties.insert(
53            "stroke_color".to_string(),
54            JsonValue::String(area.stroke.color.to_hex()),
55        );
56        properties.insert(
57            "stroke_width".to_string(),
58            JsonValue::from(area.stroke.width),
59        );
60        properties.insert(
61            "fill_color".to_string(),
62            JsonValue::String(area.fill.to_hex()),
63        );
64        properties.insert(
65            "fill_type".to_string(),
66            JsonValue::String(
67                match area.fill_type {
68                    FillType::None => "None",
69                    FillType::Solid => "Solid",
70                    FillType::Hatching => "Hatching",
71                }
72                .to_string(),
73            ),
74        );
75
76        match area.shape {
77            AreaShape::Polygon(points) => {
78                let polygon_points: Vec<Vec<geojson::Position>> = vec![
79                    points
80                        .iter()
81                        // GeoJSON polygons must be closed, so the first and last points must be the same.
82                        .chain(points.first())
83                        .map(|gp| geojson::Position::from(vec![gp.lon, gp.lat]))
84                        .collect(),
85                ];
86                feature.geometry = Some(Geometry::new(GeometryValue::Polygon {
87                    coordinates: polygon_points,
88                }));
89            }
90            AreaShape::Circle {
91                center,
92                radius,
93                points,
94            } => {
95                let point = Geometry::new(GeometryValue::Point {
96                    coordinates: geojson::Position::from(vec![center.lon, center.lat]),
97                });
98                feature.geometry = Some(point);
99                properties.insert("radius".to_string(), JsonValue::from(radius));
100                if let Some(p) = points {
101                    properties.insert("points".to_string(), JsonValue::from(p));
102                }
103            }
104            AreaShape::Ellipse {
105                center,
106                radius_major,
107                radius_minor,
108                rotation,
109                points,
110            } => {
111                let point = Geometry::new(GeometryValue::Point {
112                    coordinates: geojson::Position::from(vec![center.lon, center.lat]),
113                });
114                feature.geometry = Some(point);
115                properties.insert("radius_major".to_string(), JsonValue::from(radius_major));
116                properties.insert("radius_minor".to_string(), JsonValue::from(radius_minor));
117                properties.insert("rotation".to_string(), JsonValue::from(rotation));
118                if let Some(p) = points {
119                    properties.insert("points".to_string(), JsonValue::from(p));
120                }
121            }
122        }
123
124        feature.properties = Some(properties);
125        feature
126    }
127}
128
129impl TryFrom<Feature> for Area {
130    type Error = String;
131
132    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
133        let shape = if let Some(geometry) = &feature.geometry {
134            match &geometry.value {
135                GeometryValue::Polygon {
136                    coordinates: points,
137                } => {
138                    let mut polygon_points: Vec<GeoPos> = points
139                        .first()
140                        .ok_or("Polygon has no rings")?
141                        .iter()
142                        .map(|pos| GeoPos {
143                            lon: pos[0],
144                            lat: pos[1],
145                        })
146                        .collect();
147
148                    // Remove the closing point, as AreaShape::Polygon doesn't expect it.
149                    if polygon_points.first() == polygon_points.last() {
150                        polygon_points.pop();
151                    }
152
153                    Some(AreaShape::Polygon(polygon_points))
154                }
155                GeometryValue::Point { coordinates: point } => {
156                    let properties = feature
157                        .properties
158                        .as_ref()
159                        .ok_or("Feature has no properties")?;
160                    let center = GeoPos {
161                        lon: point[0],
162                        lat: point[1],
163                    };
164                    let points = properties.get("points").and_then(serde_json::Value::as_i64);
165
166                    if properties.contains_key("radius_major")
167                        || properties.contains_key("radius_minor")
168                    {
169                        let radius_major = properties
170                            .get("radius_major")
171                            .and_then(serde_json::Value::as_f64)
172                            .unwrap_or_default();
173                        let radius_minor = properties
174                            .get("radius_minor")
175                            .and_then(serde_json::Value::as_f64)
176                            .unwrap_or_default();
177                        let rotation = properties
178                            .get("rotation")
179                            .and_then(serde_json::Value::as_f64)
180                            .unwrap_or_default();
181
182                        if radius_major <= 0.0 || radius_minor <= 0.0 {
183                            return Err("Radii must be greater than 0".to_string());
184                        }
185
186                        Some(AreaShape::Ellipse {
187                            center,
188                            radius_major,
189                            radius_minor,
190                            rotation,
191                            points,
192                        })
193                    } else {
194                        let radius = properties
195                            .get("radius")
196                            .and_then(serde_json::Value::as_f64)
197                            .unwrap_or_default();
198
199                        if radius <= 0.0 {
200                            return Err("Radius must be greater than 0".to_string());
201                        }
202
203                        Some(AreaShape::Circle {
204                            center,
205                            radius,
206                            points,
207                        })
208                    }
209                }
210                _ => None,
211            }
212        } else {
213            None
214        };
215
216        let shape = shape.ok_or("Unsupported geometry or missing shape data")?;
217
218        // default stroke and fill settings to use if not present in the feature properties
219        let mut stroke = Stroke::new(1.0, Color32::RED);
220        let mut fill = Color32::TRANSPARENT;
221
222        if let Some(properties) = &feature.properties {
223            check_version_from_properties(properties);
224            if let Some(value) = properties.get("stroke_width")
225                && let Some(width) = value.as_f64()
226            {
227                stroke.width = width as f32;
228            }
229            if let Some(value) = properties.get("stroke_color")
230                && let Some(s) = value.as_str()
231                && let Ok(color) = Color32::from_hex(s)
232            {
233                stroke.color = color;
234            }
235            if let Some(value) = properties.get("fill_color")
236                && let Some(s) = value.as_str()
237                && let Ok(color) = Color32::from_hex(s)
238            {
239                fill = color;
240            }
241        }
242
243        let fill_type = if let Some(properties) = &feature.properties {
244            match properties.get("fill_type").and_then(|v| v.as_str()) {
245                Some("None") => FillType::None,
246                Some("Hatching") => FillType::Hatching,
247                _ => FillType::Solid, // Default for backwards compatibility
248            }
249        } else {
250            FillType::Solid
251        };
252
253        Ok(Area {
254            shape,
255            stroke,
256            fill,
257            fill_type,
258        })
259    }
260}
261
262impl From<Polyline> for Feature {
263    fn from(polyline: Polyline) -> Self {
264        let mut feature = Feature::default();
265        let mut properties = Map::new();
266        add_version_to_properties(&mut properties);
267        feature.properties = Some(properties);
268        let line_string: Vec<geojson::Position> = polyline
269            .0
270            .iter()
271            .map(|gp| geojson::Position::from(vec![gp.lon, gp.lat]))
272            .collect();
273        feature.geometry = Some(Geometry::new(GeometryValue::LineString {
274            coordinates: line_string,
275        }));
276        feature
277    }
278}
279
280impl TryFrom<Feature> for Polyline {
281    type Error = String;
282
283    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
284        if let Some(geometry) = feature.geometry
285            && let GeometryValue::LineString {
286                coordinates: line_string,
287            } = geometry.value
288        {
289            return Ok(Polyline(
290                line_string
291                    .iter()
292                    .map(|pos| GeoPos {
293                        lon: pos[0],
294                        lat: pos[1],
295                    })
296                    .collect(),
297            ));
298        }
299        if let Some(properties) = &feature.properties {
300            check_version_from_properties(properties);
301        }
302        Err("Feature is not a LineString".to_string())
303    }
304}
305
306impl From<Text> for Feature {
307    fn from(text: Text) -> Self {
308        let mut feature = Feature::default();
309        let mut properties = Map::new();
310        add_version_to_properties(&mut properties);
311        let point = Geometry::new(GeometryValue::Point {
312            coordinates: geojson::Position::from(vec![text.pos.lon, text.pos.lat]),
313        });
314        feature.geometry = Some(point);
315        properties.insert("text".to_string(), JsonValue::String(text.text));
316        properties.insert("color".to_string(), JsonValue::String(text.color.to_hex()));
317        properties.insert(
318            "background".to_string(),
319            JsonValue::String(text.background.to_hex()),
320        );
321
322        match text.size {
323            TextSize::Static(size) => {
324                properties.insert(
325                    "size_type".to_string(),
326                    JsonValue::String("Static".to_string()),
327                );
328                properties.insert("size".to_string(), JsonValue::from(size));
329            }
330            TextSize::Relative(size) => {
331                properties.insert(
332                    "size_type".to_string(),
333                    JsonValue::String("Relative".to_string()),
334                );
335                properties.insert("size".to_string(), JsonValue::from(size));
336            }
337        }
338
339        feature.properties = Some(properties);
340        feature
341    }
342}
343
344impl TryFrom<Feature> for Text {
345    type Error = String;
346
347    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
348        let mut text = Text::default();
349        if let Some(geometry) = feature.geometry {
350            if let GeometryValue::Point { coordinates: point } = geometry.value {
351                text.pos = GeoPos {
352                    lon: point[0],
353                    lat: point[1],
354                };
355            } else {
356                return Err("Feature is not a Point".to_string());
357            }
358        } else {
359            return Err("Feature has no geometry".to_string());
360        }
361
362        if let Some(properties) = feature.properties {
363            check_version_from_properties(&properties);
364            if let Some(value) = properties.get("text") {
365                if let Some(s) = value.as_str() {
366                    text.text = s.to_string();
367                } else {
368                    return Err("Property 'text' is not a string".to_string());
369                }
370            } else {
371                return Err("Feature has no 'text' property".to_string());
372            }
373            if let Some(value) = properties.get("color")
374                && let Some(s) = value.as_str()
375                && let Ok(color) = Color32::from_hex(s)
376            {
377                text.color = color;
378            }
379            if let Some(value) = properties.get("background")
380                && let Some(s) = value.as_str()
381                && let Ok(color) = Color32::from_hex(s)
382            {
383                text.background = color;
384            }
385            if let Some(size_type) = properties.get("size_type")
386                && let Some(size) = properties.get("size")
387                && let Some(size_f32) = size.as_f64()
388            {
389                if size_type == "Static" {
390                    text.size = TextSize::Static(size_f32 as f32);
391                } else if size_type == "Relative" {
392                    text.size = TextSize::Relative(size_f32 as f32);
393                }
394            }
395        }
396        Ok(text)
397    }
398}