Skip to main content

rustial_engine/
geojson.rs

1//! GeoJSON parser.
2//!
3//! Parses GeoJSON strings into internal geometry types.
4
5use crate::geometry::*;
6use rustial_math::GeoCoord;
7use thiserror::Error;
8
9/// Error type for GeoJSON parsing.
10#[derive(Debug, Error)]
11pub enum GeoJsonError {
12    /// Failed to parse the JSON input.
13    #[error("JSON parse error: {0}")]
14    Json(String),
15    /// The GeoJSON structure is invalid or unsupported.
16    #[error("invalid GeoJSON structure: {0}")]
17    Structure(String),
18}
19
20#[cfg(feature = "geojson")]
21impl From<serde_json::Error> for GeoJsonError {
22    fn from(e: serde_json::Error) -> Self {
23        GeoJsonError::Json(e.to_string())
24    }
25}
26
27/// Parse a GeoJSON string into a [`FeatureCollection`].
28#[cfg(feature = "geojson")]
29pub fn parse_geojson(input: &str) -> Result<FeatureCollection, GeoJsonError> {
30    let value: serde_json::Value = serde_json::from_str(input)?;
31    parse_value(&value)
32}
33
34#[cfg(feature = "geojson")]
35fn parse_value(value: &serde_json::Value) -> Result<FeatureCollection, GeoJsonError> {
36    let obj = value
37        .as_object()
38        .ok_or_else(|| GeoJsonError::Structure("expected object".into()))?;
39
40    let type_str = obj
41        .get("type")
42        .and_then(|v| v.as_str())
43        .ok_or_else(|| GeoJsonError::Structure("missing 'type'".into()))?;
44
45    match type_str {
46        "FeatureCollection" => {
47            let features_arr = obj
48                .get("features")
49                .and_then(|v| v.as_array())
50                .ok_or_else(|| GeoJsonError::Structure("missing 'features' array".into()))?;
51
52            let mut features = Vec::with_capacity(features_arr.len());
53            for f in features_arr {
54                features.push(parse_feature(f)?);
55            }
56            Ok(FeatureCollection { features })
57        }
58        "Feature" => {
59            let feature = parse_feature(value)?;
60            Ok(FeatureCollection {
61                features: vec![feature],
62            })
63        }
64        _ => {
65            // Try to parse as a bare geometry.
66            let geom = parse_geometry(value)?;
67            Ok(FeatureCollection {
68                features: vec![Feature {
69                    geometry: geom,
70                    properties: std::collections::HashMap::new(),
71                }],
72            })
73        }
74    }
75}
76
77#[cfg(feature = "geojson")]
78fn parse_feature(value: &serde_json::Value) -> Result<Feature, GeoJsonError> {
79    let obj = value
80        .as_object()
81        .ok_or_else(|| GeoJsonError::Structure("feature is not an object".into()))?;
82
83    let geom_value = obj
84        .get("geometry")
85        .ok_or_else(|| GeoJsonError::Structure("missing 'geometry'".into()))?;
86
87    let geometry = parse_geometry(geom_value)?;
88
89    let mut properties = std::collections::HashMap::new();
90    if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
91        for (key, val) in props {
92            let pv = match val {
93                serde_json::Value::Null => PropertyValue::Null,
94                serde_json::Value::Bool(b) => PropertyValue::Bool(*b),
95                serde_json::Value::Number(n) => PropertyValue::Number(n.as_f64().unwrap_or(0.0)),
96                serde_json::Value::String(s) => PropertyValue::String(s.clone()),
97                _ => PropertyValue::String(val.to_string()),
98            };
99            properties.insert(key.clone(), pv);
100        }
101    }
102
103    Ok(Feature {
104        geometry,
105        properties,
106    })
107}
108
109#[cfg(feature = "geojson")]
110fn parse_geometry(value: &serde_json::Value) -> Result<Geometry, GeoJsonError> {
111    let obj = value
112        .as_object()
113        .ok_or_else(|| GeoJsonError::Structure("geometry is not an object".into()))?;
114
115    let type_str = obj
116        .get("type")
117        .and_then(|v| v.as_str())
118        .ok_or_else(|| GeoJsonError::Structure("geometry missing 'type'".into()))?;
119
120    match type_str {
121        "Point" => {
122            let coords = get_coord_array(obj, "coordinates")?;
123            if coords.len() < 2 {
124                return Err(GeoJsonError::Structure("Point needs 2+ coords".into()));
125            }
126            Ok(Geometry::Point(Point {
127                coord: arr_to_geo(&coords),
128            }))
129        }
130        "LineString" => {
131            let coords = get_coord_arrays(obj, "coordinates")?;
132            let line = coords.iter().map(|c| arr_to_geo(c)).collect();
133            Ok(Geometry::LineString(LineString { coords: line }))
134        }
135        "Polygon" => {
136            let rings = get_rings(obj, "coordinates")?;
137            let exterior = rings
138                .first()
139                .ok_or_else(|| GeoJsonError::Structure("polygon needs at least one ring".into()))?
140                .iter()
141                .map(|c| arr_to_geo(c))
142                .collect();
143            let interiors = rings[1..]
144                .iter()
145                .map(|ring| ring.iter().map(|c| arr_to_geo(c)).collect())
146                .collect();
147            Ok(Geometry::Polygon(Polygon {
148                exterior,
149                interiors,
150            }))
151        }
152        "MultiPoint" => {
153            let coords = get_coord_arrays(obj, "coordinates")?;
154            let points = coords
155                .iter()
156                .map(|c| Point {
157                    coord: arr_to_geo(c),
158                })
159                .collect();
160            Ok(Geometry::MultiPoint(MultiPoint { points }))
161        }
162        "MultiLineString" => {
163            let rings = get_rings(obj, "coordinates")?;
164            let lines = rings
165                .iter()
166                .map(|ring| LineString {
167                    coords: ring.iter().map(|c| arr_to_geo(c)).collect(),
168                })
169                .collect();
170            Ok(Geometry::MultiLineString(MultiLineString { lines }))
171        }
172        "MultiPolygon" => {
173            let polys_raw = obj
174                .get("coordinates")
175                .and_then(|v| v.as_array())
176                .ok_or_else(|| GeoJsonError::Structure("missing 'coordinates'".into()))?;
177
178            let mut polygons = Vec::new();
179            for poly_val in polys_raw {
180                let rings: Vec<Vec<Vec<f64>>> = poly_val
181                    .as_array()
182                    .ok_or_else(|| GeoJsonError::Structure("invalid polygon ring".into()))?
183                    .iter()
184                    .map(|ring| {
185                        ring.as_array()
186                            .unwrap_or(&Vec::new())
187                            .iter()
188                            .map(|c| {
189                                c.as_array()
190                                    .unwrap_or(&Vec::new())
191                                    .iter()
192                                    .filter_map(|v| v.as_f64())
193                                    .collect()
194                            })
195                            .collect()
196                    })
197                    .collect();
198
199                let exterior = rings
200                    .first()
201                    .map(|r| r.iter().map(|c| arr_to_geo(c)).collect())
202                    .unwrap_or_default();
203                let interiors = rings[1..]
204                    .iter()
205                    .map(|ring| ring.iter().map(|c| arr_to_geo(c)).collect())
206                    .collect();
207                polygons.push(Polygon {
208                    exterior,
209                    interiors,
210                });
211            }
212            Ok(Geometry::MultiPolygon(MultiPolygon { polygons }))
213        }
214        "GeometryCollection" => {
215            let geometries = obj
216                .get("geometries")
217                .and_then(|v| v.as_array())
218                .ok_or_else(|| GeoJsonError::Structure("missing 'geometries'".into()))?;
219            let mut geoms = Vec::new();
220            for g in geometries {
221                geoms.push(parse_geometry(g)?);
222            }
223            Ok(Geometry::GeometryCollection(geoms))
224        }
225        other => Err(GeoJsonError::Structure(format!(
226            "unknown geometry type: {other}"
227        ))),
228    }
229}
230
231#[cfg(feature = "geojson")]
232fn arr_to_geo(arr: &[f64]) -> GeoCoord {
233    let lon = if !arr.is_empty() { arr[0] } else { 0.0 };
234    let lat = if arr.len() > 1 { arr[1] } else { 0.0 };
235    let alt = if arr.len() > 2 { arr[2] } else { 0.0 };
236    GeoCoord::new(lat, lon, alt)
237}
238
239#[cfg(feature = "geojson")]
240fn get_coord_array(
241    obj: &serde_json::Map<String, serde_json::Value>,
242    key: &str,
243) -> Result<Vec<f64>, GeoJsonError> {
244    obj.get(key)
245        .and_then(|v| v.as_array())
246        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}' array")))?
247        .iter()
248        .map(|v| {
249            v.as_f64()
250                .ok_or_else(|| GeoJsonError::Structure("coordinate is not a number".into()))
251        })
252        .collect()
253}
254
255#[cfg(feature = "geojson")]
256fn get_coord_arrays(
257    obj: &serde_json::Map<String, serde_json::Value>,
258    key: &str,
259) -> Result<Vec<Vec<f64>>, GeoJsonError> {
260    obj.get(key)
261        .and_then(|v| v.as_array())
262        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}'")))?
263        .iter()
264        .map(|v| {
265            v.as_array()
266                .ok_or_else(|| GeoJsonError::Structure("expected array of coords".into()))?
267                .iter()
268                .map(|n| {
269                    n.as_f64()
270                        .ok_or_else(|| GeoJsonError::Structure("coord not a number".into()))
271                })
272                .collect()
273        })
274        .collect()
275}
276
277#[cfg(feature = "geojson")]
278fn get_rings(
279    obj: &serde_json::Map<String, serde_json::Value>,
280    key: &str,
281) -> Result<Vec<Vec<Vec<f64>>>, GeoJsonError> {
282    obj.get(key)
283        .and_then(|v| v.as_array())
284        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}'")))?
285        .iter()
286        .map(|ring| {
287            ring.as_array()
288                .ok_or_else(|| GeoJsonError::Structure("ring is not an array".into()))?
289                .iter()
290                .map(|coord| {
291                    coord
292                        .as_array()
293                        .ok_or_else(|| GeoJsonError::Structure("coord is not an array".into()))?
294                        .iter()
295                        .map(|n| {
296                            n.as_f64().ok_or_else(|| {
297                                GeoJsonError::Structure("coord component not a number".into())
298                            })
299                        })
300                        .collect()
301                })
302                .collect()
303        })
304        .collect()
305}
306
307#[cfg(test)]
308#[cfg(feature = "geojson")]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn parse_point() {
314        let json = r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[17.0,51.1]},"properties":{"name":"Wroclaw"}}"#;
315        let fc = parse_geojson(json).unwrap();
316        assert_eq!(fc.len(), 1);
317        match &fc.features[0].geometry {
318            Geometry::Point(p) => {
319                assert!((p.coord.lat - 51.1).abs() < 1e-6);
320                assert!((p.coord.lon - 17.0).abs() < 1e-6);
321            }
322            _ => panic!("expected Point"),
323        }
324    }
325
326    #[test]
327    fn parse_feature_collection() {
328        let json = r#"{
329            "type": "FeatureCollection",
330            "features": [
331                {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{}},
332                {"type":"Feature","geometry":{"type":"Point","coordinates":[1,1]},"properties":{}}
333            ]
334        }"#;
335        let fc = parse_geojson(json).unwrap();
336        assert_eq!(fc.len(), 2);
337    }
338
339    #[test]
340    fn parse_polygon() {
341        let json = r#"{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]},"properties":{}}"#;
342        let fc = parse_geojson(json).unwrap();
343        match &fc.features[0].geometry {
344            Geometry::Polygon(p) => {
345                assert_eq!(p.exterior.len(), 5);
346                assert!(p.interiors.is_empty());
347            }
348            _ => panic!("expected Polygon"),
349        }
350    }
351
352    #[test]
353    fn parse_linestring() {
354        let json = r#"{"type":"Feature","geometry":{"type":"LineString","coordinates":[[0,0],[1,1],[2,2]]},"properties":{}}"#;
355        let fc = parse_geojson(json).unwrap();
356        match &fc.features[0].geometry {
357            Geometry::LineString(ls) => {
358                assert_eq!(ls.coords.len(), 3);
359            }
360            _ => panic!("expected LineString"),
361        }
362    }
363}