rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! GeoJSON parser.
//!
//! Parses GeoJSON strings into internal geometry types.

use crate::geometry::*;
use rustial_math::GeoCoord;
use thiserror::Error;

/// Error type for GeoJSON parsing.
#[derive(Debug, Error)]
pub enum GeoJsonError {
    /// Failed to parse the JSON input.
    #[error("JSON parse error: {0}")]
    Json(String),
    /// The GeoJSON structure is invalid or unsupported.
    #[error("invalid GeoJSON structure: {0}")]
    Structure(String),
}

#[cfg(feature = "geojson")]
impl From<serde_json::Error> for GeoJsonError {
    fn from(e: serde_json::Error) -> Self {
        GeoJsonError::Json(e.to_string())
    }
}

/// Parse a GeoJSON string into a [`FeatureCollection`].
#[cfg(feature = "geojson")]
pub fn parse_geojson(input: &str) -> Result<FeatureCollection, GeoJsonError> {
    let value: serde_json::Value = serde_json::from_str(input)?;
    parse_value(&value)
}

#[cfg(feature = "geojson")]
fn parse_value(value: &serde_json::Value) -> Result<FeatureCollection, GeoJsonError> {
    let obj = value
        .as_object()
        .ok_or_else(|| GeoJsonError::Structure("expected object".into()))?;

    let type_str = obj
        .get("type")
        .and_then(|v| v.as_str())
        .ok_or_else(|| GeoJsonError::Structure("missing 'type'".into()))?;

    match type_str {
        "FeatureCollection" => {
            let features_arr = obj
                .get("features")
                .and_then(|v| v.as_array())
                .ok_or_else(|| GeoJsonError::Structure("missing 'features' array".into()))?;

            let mut features = Vec::with_capacity(features_arr.len());
            for f in features_arr {
                features.push(parse_feature(f)?);
            }
            Ok(FeatureCollection { features })
        }
        "Feature" => {
            let feature = parse_feature(value)?;
            Ok(FeatureCollection {
                features: vec![feature],
            })
        }
        _ => {
            // Try to parse as a bare geometry.
            let geom = parse_geometry(value)?;
            Ok(FeatureCollection {
                features: vec![Feature {
                    geometry: geom,
                    properties: std::collections::HashMap::new(),
                }],
            })
        }
    }
}

#[cfg(feature = "geojson")]
fn parse_feature(value: &serde_json::Value) -> Result<Feature, GeoJsonError> {
    let obj = value
        .as_object()
        .ok_or_else(|| GeoJsonError::Structure("feature is not an object".into()))?;

    let geom_value = obj
        .get("geometry")
        .ok_or_else(|| GeoJsonError::Structure("missing 'geometry'".into()))?;

    let geometry = parse_geometry(geom_value)?;

    let mut properties = std::collections::HashMap::new();
    if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
        for (key, val) in props {
            let pv = match val {
                serde_json::Value::Null => PropertyValue::Null,
                serde_json::Value::Bool(b) => PropertyValue::Bool(*b),
                serde_json::Value::Number(n) => PropertyValue::Number(n.as_f64().unwrap_or(0.0)),
                serde_json::Value::String(s) => PropertyValue::String(s.clone()),
                _ => PropertyValue::String(val.to_string()),
            };
            properties.insert(key.clone(), pv);
        }
    }

    Ok(Feature {
        geometry,
        properties,
    })
}

#[cfg(feature = "geojson")]
fn parse_geometry(value: &serde_json::Value) -> Result<Geometry, GeoJsonError> {
    let obj = value
        .as_object()
        .ok_or_else(|| GeoJsonError::Structure("geometry is not an object".into()))?;

    let type_str = obj
        .get("type")
        .and_then(|v| v.as_str())
        .ok_or_else(|| GeoJsonError::Structure("geometry missing 'type'".into()))?;

    match type_str {
        "Point" => {
            let coords = get_coord_array(obj, "coordinates")?;
            if coords.len() < 2 {
                return Err(GeoJsonError::Structure("Point needs 2+ coords".into()));
            }
            Ok(Geometry::Point(Point {
                coord: arr_to_geo(&coords),
            }))
        }
        "LineString" => {
            let coords = get_coord_arrays(obj, "coordinates")?;
            let line = coords.iter().map(|c| arr_to_geo(c)).collect();
            Ok(Geometry::LineString(LineString { coords: line }))
        }
        "Polygon" => {
            let rings = get_rings(obj, "coordinates")?;
            let exterior = rings
                .first()
                .ok_or_else(|| GeoJsonError::Structure("polygon needs at least one ring".into()))?
                .iter()
                .map(|c| arr_to_geo(c))
                .collect();
            let interiors = rings[1..]
                .iter()
                .map(|ring| ring.iter().map(|c| arr_to_geo(c)).collect())
                .collect();
            Ok(Geometry::Polygon(Polygon {
                exterior,
                interiors,
            }))
        }
        "MultiPoint" => {
            let coords = get_coord_arrays(obj, "coordinates")?;
            let points = coords
                .iter()
                .map(|c| Point {
                    coord: arr_to_geo(c),
                })
                .collect();
            Ok(Geometry::MultiPoint(MultiPoint { points }))
        }
        "MultiLineString" => {
            let rings = get_rings(obj, "coordinates")?;
            let lines = rings
                .iter()
                .map(|ring| LineString {
                    coords: ring.iter().map(|c| arr_to_geo(c)).collect(),
                })
                .collect();
            Ok(Geometry::MultiLineString(MultiLineString { lines }))
        }
        "MultiPolygon" => {
            let polys_raw = obj
                .get("coordinates")
                .and_then(|v| v.as_array())
                .ok_or_else(|| GeoJsonError::Structure("missing 'coordinates'".into()))?;

            let mut polygons = Vec::new();
            for poly_val in polys_raw {
                let rings: Vec<Vec<Vec<f64>>> = poly_val
                    .as_array()
                    .ok_or_else(|| GeoJsonError::Structure("invalid polygon ring".into()))?
                    .iter()
                    .map(|ring| {
                        ring.as_array()
                            .unwrap_or(&Vec::new())
                            .iter()
                            .map(|c| {
                                c.as_array()
                                    .unwrap_or(&Vec::new())
                                    .iter()
                                    .filter_map(|v| v.as_f64())
                                    .collect()
                            })
                            .collect()
                    })
                    .collect();

                let exterior = rings
                    .first()
                    .map(|r| r.iter().map(|c| arr_to_geo(c)).collect())
                    .unwrap_or_default();
                let interiors = rings[1..]
                    .iter()
                    .map(|ring| ring.iter().map(|c| arr_to_geo(c)).collect())
                    .collect();
                polygons.push(Polygon {
                    exterior,
                    interiors,
                });
            }
            Ok(Geometry::MultiPolygon(MultiPolygon { polygons }))
        }
        "GeometryCollection" => {
            let geometries = obj
                .get("geometries")
                .and_then(|v| v.as_array())
                .ok_or_else(|| GeoJsonError::Structure("missing 'geometries'".into()))?;
            let mut geoms = Vec::new();
            for g in geometries {
                geoms.push(parse_geometry(g)?);
            }
            Ok(Geometry::GeometryCollection(geoms))
        }
        other => Err(GeoJsonError::Structure(format!(
            "unknown geometry type: {other}"
        ))),
    }
}

#[cfg(feature = "geojson")]
fn arr_to_geo(arr: &[f64]) -> GeoCoord {
    let lon = if !arr.is_empty() { arr[0] } else { 0.0 };
    let lat = if arr.len() > 1 { arr[1] } else { 0.0 };
    let alt = if arr.len() > 2 { arr[2] } else { 0.0 };
    GeoCoord::new(lat, lon, alt)
}

#[cfg(feature = "geojson")]
fn get_coord_array(
    obj: &serde_json::Map<String, serde_json::Value>,
    key: &str,
) -> Result<Vec<f64>, GeoJsonError> {
    obj.get(key)
        .and_then(|v| v.as_array())
        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}' array")))?
        .iter()
        .map(|v| {
            v.as_f64()
                .ok_or_else(|| GeoJsonError::Structure("coordinate is not a number".into()))
        })
        .collect()
}

#[cfg(feature = "geojson")]
fn get_coord_arrays(
    obj: &serde_json::Map<String, serde_json::Value>,
    key: &str,
) -> Result<Vec<Vec<f64>>, GeoJsonError> {
    obj.get(key)
        .and_then(|v| v.as_array())
        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}'")))?
        .iter()
        .map(|v| {
            v.as_array()
                .ok_or_else(|| GeoJsonError::Structure("expected array of coords".into()))?
                .iter()
                .map(|n| {
                    n.as_f64()
                        .ok_or_else(|| GeoJsonError::Structure("coord not a number".into()))
                })
                .collect()
        })
        .collect()
}

#[cfg(feature = "geojson")]
fn get_rings(
    obj: &serde_json::Map<String, serde_json::Value>,
    key: &str,
) -> Result<Vec<Vec<Vec<f64>>>, GeoJsonError> {
    obj.get(key)
        .and_then(|v| v.as_array())
        .ok_or_else(|| GeoJsonError::Structure(format!("missing '{key}'")))?
        .iter()
        .map(|ring| {
            ring.as_array()
                .ok_or_else(|| GeoJsonError::Structure("ring is not an array".into()))?
                .iter()
                .map(|coord| {
                    coord
                        .as_array()
                        .ok_or_else(|| GeoJsonError::Structure("coord is not an array".into()))?
                        .iter()
                        .map(|n| {
                            n.as_f64().ok_or_else(|| {
                                GeoJsonError::Structure("coord component not a number".into())
                            })
                        })
                        .collect()
                })
                .collect()
        })
        .collect()
}

#[cfg(test)]
#[cfg(feature = "geojson")]
mod tests {
    use super::*;

    #[test]
    fn parse_point() {
        let json = r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[17.0,51.1]},"properties":{"name":"Wroclaw"}}"#;
        let fc = parse_geojson(json).unwrap();
        assert_eq!(fc.len(), 1);
        match &fc.features[0].geometry {
            Geometry::Point(p) => {
                assert!((p.coord.lat - 51.1).abs() < 1e-6);
                assert!((p.coord.lon - 17.0).abs() < 1e-6);
            }
            _ => panic!("expected Point"),
        }
    }

    #[test]
    fn parse_feature_collection() {
        let json = r#"{
            "type": "FeatureCollection",
            "features": [
                {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{}},
                {"type":"Feature","geometry":{"type":"Point","coordinates":[1,1]},"properties":{}}
            ]
        }"#;
        let fc = parse_geojson(json).unwrap();
        assert_eq!(fc.len(), 2);
    }

    #[test]
    fn parse_polygon() {
        let json = r#"{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]},"properties":{}}"#;
        let fc = parse_geojson(json).unwrap();
        match &fc.features[0].geometry {
            Geometry::Polygon(p) => {
                assert_eq!(p.exterior.len(), 5);
                assert!(p.interiors.is_empty());
            }
            _ => panic!("expected Polygon"),
        }
    }

    #[test]
    fn parse_linestring() {
        let json = r#"{"type":"Feature","geometry":{"type":"LineString","coordinates":[[0,0],[1,1],[2,2]]},"properties":{}}"#;
        let fc = parse_geojson(json).unwrap();
        match &fc.features[0].geometry {
            Geometry::LineString(ls) => {
                assert_eq!(ls.coords.len(), 3);
            }
            _ => panic!("expected LineString"),
        }
    }
}