use crate::geometry::*;
use rustial_math::GeoCoord;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GeoJsonError {
#[error("JSON parse error: {0}")]
Json(String),
#[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())
}
}
#[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],
})
}
_ => {
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"),
}
}
}