use super::format::Format;
use crate::core::{
EventBuilder, Location, Narrative, NarrativeBuilder, SourceRef, SourceType, Timestamp,
};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::io::{Read, Write};
#[derive(Debug, Clone, Default)]
pub struct GeoJsonFormat {
pub options: GeoJsonOptions,
}
#[derive(Debug, Clone)]
pub struct GeoJsonOptions {
pub include_ids: bool,
pub include_tags: bool,
pub include_sources: bool,
pub timestamp_property: String,
pub text_property: String,
}
impl Default for GeoJsonOptions {
fn default() -> Self {
Self {
include_ids: true,
include_tags: true,
include_sources: true,
timestamp_property: "timestamp".to_string(),
text_property: "text".to_string(),
}
}
}
impl GeoJsonFormat {
pub fn new() -> Self {
Self::default()
}
pub fn with_options(options: GeoJsonOptions) -> Self {
Self { options }
}
}
#[derive(Debug, Serialize, Deserialize)]
struct FeatureCollection {
#[serde(rename = "type")]
type_: String,
features: Vec<Feature>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<Map<String, Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Feature {
#[serde(rename = "type")]
type_: String,
geometry: Geometry,
properties: Map<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Geometry {
#[serde(rename = "type")]
type_: String,
coordinates: Vec<f64>,
}
impl Format for GeoJsonFormat {
fn import<R: Read>(&self, reader: R) -> Result<Narrative> {
let fc: FeatureCollection = serde_json::from_reader(reader)?;
if fc.type_ != "FeatureCollection" {
return Err(Error::InvalidFormat(
"expected GeoJSON FeatureCollection".to_string(),
));
}
let mut builder = NarrativeBuilder::new();
if let Some(props) = fc.properties {
if let Some(title) = props.get("title").and_then(|v| v.as_str()) {
builder = builder.title(title);
}
if let Some(desc) = props.get("description").and_then(|v| v.as_str()) {
builder = builder.description(desc);
}
}
for feature in fc.features {
if feature.geometry.type_ != "Point" {
continue; }
let coords = &feature.geometry.coordinates;
if coords.len() < 2 {
continue; }
let lon = coords[0];
let lat = coords[1];
let mut location = Location::new(lat, lon);
if let Some(elev) = coords.get(2).copied() {
location.elevation = Some(elev);
}
let props = &feature.properties;
let timestamp = if let Some(ts_str) = props
.get(&self.options.timestamp_property)
.and_then(|v| v.as_str())
{
Timestamp::parse(ts_str)
.map_err(|e| Error::InvalidFormat(format!("invalid timestamp: {}", e)))?
} else {
Timestamp::now() };
let mut event_builder = EventBuilder::new().location(location).timestamp(timestamp);
if let Some(text) = props
.get(&self.options.text_property)
.and_then(|v| v.as_str())
{
event_builder = event_builder.text(text);
}
if let Some(tags) = props.get("tags").and_then(|v| v.as_array()) {
for tag in tags {
if let Some(tag_str) = tag.as_str() {
event_builder = event_builder.tag(tag_str);
}
}
}
if let Some(source_obj) = props.get("source").and_then(|v| v.as_object()) {
let source_type = source_obj
.get("type")
.and_then(|v| v.as_str())
.and_then(|s| match s.to_lowercase().as_str() {
"article" => Some(SourceType::Article),
"report" => Some(SourceType::Report),
"witness" => Some(SourceType::Witness),
"sensor" => Some(SourceType::Sensor),
_ => None,
})
.unwrap_or(SourceType::Article);
let mut source = SourceRef::new(source_type);
if let Some(url) = source_obj.get("url").and_then(|v| v.as_str()) {
source.url = Some(url.to_string());
}
if let Some(title) = source_obj.get("title").and_then(|v| v.as_str()) {
source.title = Some(title.to_string());
}
event_builder = event_builder.source(source);
}
let event = event_builder.build();
builder = builder.event(event);
}
Ok(builder.build())
}
fn export<W: Write>(&self, narrative: &Narrative, mut writer: W) -> Result<()> {
let mut features = Vec::new();
for event in narrative.events() {
let loc = &event.location;
let coords = if let Some(elev) = loc.elevation {
vec![loc.lon, loc.lat, elev]
} else {
vec![loc.lon, loc.lat]
};
let geometry = Geometry {
type_: "Point".to_string(),
coordinates: coords,
};
let mut properties = Map::new();
properties.insert(
self.options.timestamp_property.clone(),
Value::String(event.timestamp.to_rfc3339()),
);
properties.insert(
self.options.text_property.clone(),
Value::String(event.text.clone()),
);
if self.options.include_tags && !event.tags.is_empty() {
let tags: Vec<Value> = event
.tags
.iter()
.map(|t| Value::String(t.clone()))
.collect();
properties.insert("tags".to_string(), Value::Array(tags));
}
if self.options.include_sources && !event.sources.is_empty() {
let source = &event.sources[0]; let mut source_obj = Map::new();
source_obj.insert(
"type".to_string(),
Value::String(source.source_type.to_string()),
);
if let Some(url) = &source.url {
source_obj.insert("url".to_string(), Value::String(url.clone()));
}
if let Some(title) = &source.title {
source_obj.insert("title".to_string(), Value::String(title.clone()));
}
properties.insert("source".to_string(), Value::Object(source_obj));
}
let feature = Feature {
type_: "Feature".to_string(),
geometry,
properties,
id: if self.options.include_ids {
Some(Value::String(event.id.to_string()))
} else {
None
},
};
features.push(feature);
}
let mut fc_properties = Map::new();
fc_properties.insert("title".to_string(), Value::String(narrative.title.clone()));
if let Some(desc) = &narrative.metadata.description {
fc_properties.insert("description".to_string(), Value::String(desc.clone()));
}
let fc = FeatureCollection {
type_: "FeatureCollection".to_string(),
features,
properties: if fc_properties.is_empty() {
None
} else {
Some(fc_properties)
},
};
serde_json::to_writer_pretty(&mut writer, &fc)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Event;
#[test]
fn test_geojson_import_basic() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.006, 40.7128]
},
"properties": {
"text": "Event at NYC",
"timestamp": "2024-01-15T14:30:00Z"
}
}
]
}"#;
let format = GeoJsonFormat::new();
let narrative = format.import_str(geojson).unwrap();
assert_eq!(narrative.events().len(), 1);
let event = &narrative.events()[0];
assert_eq!(event.location.lat, 40.7128);
assert_eq!(event.location.lon, -74.006);
assert_eq!(event.text.as_str(), "Event at NYC");
}
#[test]
fn test_geojson_roundtrip() {
let event = Event::builder()
.location(Location::new(40.7128, -74.006))
.timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
.text("Test event")
.tag("test")
.build();
let narrative = Narrative::builder()
.title("Test Narrative")
.event(event)
.build();
let format = GeoJsonFormat::new();
let exported = format.export_str(&narrative).unwrap();
let imported = format.import_str(&exported).unwrap();
assert_eq!(imported.events().len(), 1);
assert_eq!(imported.title, "Test Narrative");
}
#[test]
fn test_geojson_with_elevation() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-122.4194, 37.7749, 100.5]
},
"properties": {
"timestamp": "2024-01-15T14:30:00Z"
}
}
]
}"#;
let format = GeoJsonFormat::new();
let narrative = format.import_str(geojson).unwrap();
let event = &narrative.events()[0];
assert_eq!(event.location.elevation, Some(100.5));
}
}