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        }
105
106        feature.properties = Some(properties);
107        feature
108    }
109}
110
111impl TryFrom<Feature> for Area {
112    type Error = String;
113
114    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
115        let shape = if let Some(geometry) = &feature.geometry {
116            match &geometry.value {
117                GeometryValue::Polygon {
118                    coordinates: points,
119                } => {
120                    let mut polygon_points: Vec<GeoPos> = points
121                        .first()
122                        .ok_or("Polygon has no rings")?
123                        .iter()
124                        .map(|pos| GeoPos {
125                            lon: pos[0],
126                            lat: pos[1],
127                        })
128                        .collect();
129
130                    // Remove the closing point, as AreaShape::Polygon doesn't expect it.
131                    if polygon_points.first() == polygon_points.last() {
132                        polygon_points.pop();
133                    }
134
135                    Some(AreaShape::Polygon(polygon_points))
136                }
137                GeometryValue::Point { coordinates: point } => {
138                    let properties = feature
139                        .properties
140                        .as_ref()
141                        .ok_or("Feature has no properties")?;
142                    let center = GeoPos {
143                        lon: point[0],
144                        lat: point[1],
145                    };
146                    let radius = properties
147                        .get("radius")
148                        .and_then(serde_json::Value::as_f64)
149                        .unwrap_or_default();
150                    let points = properties.get("points").and_then(serde_json::Value::as_i64);
151
152                    if radius <= 0.0 {
153                        return Err("Radius must be greater than 0".to_string());
154                    }
155
156                    Some(AreaShape::Circle {
157                        center,
158                        radius,
159                        points,
160                    })
161                }
162                _ => None,
163            }
164        } else {
165            None
166        };
167
168        let shape = shape.ok_or("Unsupported geometry or missing shape data")?;
169
170        // default stroke and fill settings to use if not present in the feature properties
171        let mut stroke = Stroke::new(1.0, Color32::RED);
172        let mut fill = Color32::TRANSPARENT;
173
174        if let Some(properties) = &feature.properties {
175            check_version_from_properties(properties);
176            if let Some(value) = properties.get("stroke_width")
177                && let Some(width) = value.as_f64()
178            {
179                stroke.width = width as f32;
180            }
181            if let Some(value) = properties.get("stroke_color")
182                && let Some(s) = value.as_str()
183                && let Ok(color) = Color32::from_hex(s)
184            {
185                stroke.color = color;
186            }
187            if let Some(value) = properties.get("fill_color")
188                && let Some(s) = value.as_str()
189                && let Ok(color) = Color32::from_hex(s)
190            {
191                fill = color;
192            }
193        }
194
195        let fill_type = if let Some(properties) = &feature.properties {
196            match properties.get("fill_type").and_then(|v| v.as_str()) {
197                Some("None") => FillType::None,
198                Some("Hatching") => FillType::Hatching,
199                _ => FillType::Solid, // Default for backwards compatibility
200            }
201        } else {
202            FillType::Solid
203        };
204
205        Ok(Area {
206            shape,
207            stroke,
208            fill,
209            fill_type,
210        })
211    }
212}
213
214impl From<Polyline> for Feature {
215    fn from(polyline: Polyline) -> Self {
216        let mut feature = Feature::default();
217        let mut properties = Map::new();
218        add_version_to_properties(&mut properties);
219        feature.properties = Some(properties);
220        let line_string: Vec<geojson::Position> = polyline
221            .0
222            .iter()
223            .map(|gp| geojson::Position::from(vec![gp.lon, gp.lat]))
224            .collect();
225        feature.geometry = Some(Geometry::new(GeometryValue::LineString {
226            coordinates: line_string,
227        }));
228        feature
229    }
230}
231
232impl TryFrom<Feature> for Polyline {
233    type Error = String;
234
235    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
236        if let Some(geometry) = feature.geometry
237            && let GeometryValue::LineString {
238                coordinates: line_string,
239            } = geometry.value
240        {
241            return Ok(Polyline(
242                line_string
243                    .iter()
244                    .map(|pos| GeoPos {
245                        lon: pos[0],
246                        lat: pos[1],
247                    })
248                    .collect(),
249            ));
250        }
251        if let Some(properties) = &feature.properties {
252            check_version_from_properties(properties);
253        }
254        Err("Feature is not a LineString".to_string())
255    }
256}
257
258impl From<Text> for Feature {
259    fn from(text: Text) -> Self {
260        let mut feature = Feature::default();
261        let mut properties = Map::new();
262        add_version_to_properties(&mut properties);
263        let point = Geometry::new(GeometryValue::Point {
264            coordinates: geojson::Position::from(vec![text.pos.lon, text.pos.lat]),
265        });
266        feature.geometry = Some(point);
267        properties.insert("text".to_string(), JsonValue::String(text.text));
268        properties.insert("color".to_string(), JsonValue::String(text.color.to_hex()));
269        properties.insert(
270            "background".to_string(),
271            JsonValue::String(text.background.to_hex()),
272        );
273
274        match text.size {
275            TextSize::Static(size) => {
276                properties.insert(
277                    "size_type".to_string(),
278                    JsonValue::String("Static".to_string()),
279                );
280                properties.insert("size".to_string(), JsonValue::from(size));
281            }
282            TextSize::Relative(size) => {
283                properties.insert(
284                    "size_type".to_string(),
285                    JsonValue::String("Relative".to_string()),
286                );
287                properties.insert("size".to_string(), JsonValue::from(size));
288            }
289        }
290
291        feature.properties = Some(properties);
292        feature
293    }
294}
295
296impl TryFrom<Feature> for Text {
297    type Error = String;
298
299    fn try_from(feature: Feature) -> Result<Self, Self::Error> {
300        let mut text = Text::default();
301        if let Some(geometry) = feature.geometry {
302            if let GeometryValue::Point { coordinates: point } = geometry.value {
303                text.pos = GeoPos {
304                    lon: point[0],
305                    lat: point[1],
306                };
307            } else {
308                return Err("Feature is not a Point".to_string());
309            }
310        } else {
311            return Err("Feature has no geometry".to_string());
312        }
313
314        if let Some(properties) = feature.properties {
315            check_version_from_properties(&properties);
316            if let Some(value) = properties.get("text") {
317                if let Some(s) = value.as_str() {
318                    text.text = s.to_string();
319                } else {
320                    return Err("Property 'text' is not a string".to_string());
321                }
322            } else {
323                return Err("Feature has no 'text' property".to_string());
324            }
325            if let Some(value) = properties.get("color")
326                && let Some(s) = value.as_str()
327                && let Ok(color) = Color32::from_hex(s)
328            {
329                text.color = color;
330            }
331            if let Some(value) = properties.get("background")
332                && let Some(s) = value.as_str()
333                && let Ok(color) = Color32::from_hex(s)
334            {
335                text.background = color;
336            }
337            if let Some(size_type) = properties.get("size_type")
338                && let Some(size) = properties.get("size")
339                && let Some(size_f32) = size.as_f64()
340            {
341                if size_type == "Static" {
342                    text.size = TextSize::Static(size_f32 as f32);
343                } else if size_type == "Relative" {
344                    text.size = TextSize::Relative(size_f32 as f32);
345                }
346            }
347        }
348        Ok(text)
349    }
350}