use std::io::Write;
use geonative_core::{Feature, Schema};
use serde_json::{Map as JsonMap, Value as Json};
use crate::error::{GeoJsonError, Result};
use crate::geometry::to_json as geom_to_json;
use crate::properties::value_to_json;
#[derive(Debug)]
pub struct GeoJsonWriter<W: Write> {
inner: W,
schema: Schema,
state: State,
}
#[derive(Debug, PartialEq, Eq)]
enum State {
BeforeFirst,
Streaming,
Closed,
}
impl<W: Write> GeoJsonWriter<W> {
pub fn create(mut sink: W, schema: &Schema) -> Result<Self> {
sink.write_all(br#"{"type":"FeatureCollection","features":["#)?;
Ok(Self {
inner: sink,
schema: schema.clone(),
state: State::BeforeFirst,
})
}
pub fn write(&mut self, feat: &Feature) -> Result<()> {
if self.state == State::Closed {
return Err(GeoJsonError::malformed("writer already closed"));
}
let separator: &[u8] = if self.state == State::BeforeFirst {
self.state = State::Streaming;
&[]
} else {
b","
};
self.inner.write_all(separator)?;
let obj = feature_to_json(&self.schema, feat);
serde_json::to_writer(&mut self.inner, &obj)?;
Ok(())
}
pub fn close(mut self) -> Result<W> {
self.inner.write_all(b"]}")?;
self.state = State::Closed;
Ok(self.inner)
}
}
fn feature_to_json(schema: &Schema, feat: &Feature) -> Json {
let mut props = JsonMap::new();
for (i, field) in schema.fields.iter().enumerate() {
let v = feat
.attributes
.get(i)
.map(value_to_json)
.unwrap_or(Json::Null);
props.insert(field.name.clone(), v);
}
let mut obj = JsonMap::new();
obj.insert("type".into(), Json::String("Feature".into()));
if let Some(fid) = feat.fid {
obj.insert("id".into(), Json::Number(fid.into()));
}
obj.insert(
"geometry".into(),
feat.geometry
.as_ref()
.map(geom_to_json)
.unwrap_or(Json::Null),
);
obj.insert("properties".into(), Json::Object(props));
Json::Object(obj)
}
pub fn write_features_to_path<P, I>(path: P, schema: &Schema, features: I) -> Result<()>
where
P: AsRef<std::path::Path>,
I: IntoIterator<Item = Feature>,
{
let file = std::fs::File::create(path)?;
let mut w = GeoJsonWriter::create(std::io::BufWriter::new(file), schema)?;
for f in features {
w.write(&f)?;
}
let buf = w.close()?;
buf.into_inner()
.map_err(|e| GeoJsonError::Io(e.into_error()))?
.sync_all()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reader::GeoJsonReader;
use geonative_core::{
Coord, Crs, FieldDef, GeomField, Geometry, GeometryType, Value, ValueType,
};
fn sample_schema() -> Schema {
Schema::new(
vec![
FieldDef::new("name", ValueType::String, false),
FieldDef::new("rank", ValueType::Int64, true),
],
Some(GeomField::new("geometry", GeometryType::Point)),
Crs::Epsg(4326),
)
}
#[test]
fn writes_then_reads_back() {
let schema = sample_schema();
let features = vec![
Feature::new(
Some(1),
Some(Geometry::Point(Coord::xy(1.0, 2.0))),
vec![Value::String("alice".into()), Value::Int64(10)],
),
Feature::new(
Some(2),
Some(Geometry::Point(Coord::xy(3.0, 4.0))),
vec![Value::String("bob".into()), Value::Null],
),
];
let mut buf = Vec::new();
let mut w = GeoJsonWriter::create(&mut buf, &schema).unwrap();
for f in &features {
w.write(f).unwrap();
}
w.close().unwrap();
let r = GeoJsonReader::from_bytes(&buf).unwrap();
assert_eq!(r.feature_count(), 2);
assert_eq!(r.features()[0].fid, Some(1));
assert_eq!(r.features()[1].fid, Some(2));
assert_eq!(r.features()[1].attributes[1], Value::Null);
assert_eq!(r.features()[0].geometry, features[0].geometry);
}
#[test]
fn empty_collection_is_valid_json() {
let schema = sample_schema();
let mut buf = Vec::new();
let w = GeoJsonWriter::create(&mut buf, &schema).unwrap();
w.close().unwrap();
let v: serde_json::Value = serde_json::from_slice(&buf).unwrap();
assert_eq!(v["type"], "FeatureCollection");
assert_eq!(v["features"].as_array().unwrap().len(), 0);
}
#[test]
fn writing_after_close_errors() {
let schema = sample_schema();
let mut buf = Vec::new();
let mut w = GeoJsonWriter::create(&mut buf, &schema).unwrap();
w.write(&Feature::new(
None,
Some(Geometry::Point(Coord::xy(0.0, 0.0))),
vec![Value::String("x".into()), Value::Int64(1)],
))
.unwrap();
w.state = State::Closed;
let err = w
.write(&Feature::new(
None,
Some(Geometry::Point(Coord::xy(0.0, 0.0))),
vec![Value::String("y".into()), Value::Int64(2)],
))
.unwrap_err();
assert!(matches!(err, GeoJsonError::Malformed(_)));
}
#[test]
fn null_geometry_round_trips() {
let schema = sample_schema();
let feat = Feature::new(
None,
None,
vec![Value::String("nogeo".into()), Value::Int64(99)],
);
let mut buf = Vec::new();
let mut w = GeoJsonWriter::create(&mut buf, &schema).unwrap();
w.write(&feat).unwrap();
w.close().unwrap();
let r = GeoJsonReader::from_bytes(&buf).unwrap();
assert!(r.features()[0].geometry.is_none());
}
}