1use super::format::Format;
4use crate::core::{
5 EventBuilder, Location, Narrative, NarrativeBuilder, SourceRef, SourceType, Timestamp,
6};
7use crate::{Error, Result};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::io::{Read, Write};
11
12#[derive(Debug, Clone, Default)]
48pub struct GeoJsonFormat {
49 pub options: GeoJsonOptions,
51}
52
53#[derive(Debug, Clone)]
55pub struct GeoJsonOptions {
56 pub include_ids: bool,
58
59 pub include_tags: bool,
61
62 pub include_sources: bool,
64
65 pub timestamp_property: String,
67
68 pub text_property: String,
70}
71
72impl Default for GeoJsonOptions {
73 fn default() -> Self {
74 Self {
75 include_ids: true,
76 include_tags: true,
77 include_sources: true,
78 timestamp_property: "timestamp".to_string(),
79 text_property: "text".to_string(),
80 }
81 }
82}
83
84impl GeoJsonFormat {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn with_options(options: GeoJsonOptions) -> Self {
92 Self { options }
93 }
94}
95
96#[derive(Debug, Serialize, Deserialize)]
98struct FeatureCollection {
99 #[serde(rename = "type")]
100 type_: String,
101 features: Vec<Feature>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 properties: Option<Map<String, Value>>,
104}
105
106#[derive(Debug, Serialize, Deserialize)]
108struct Feature {
109 #[serde(rename = "type")]
110 type_: String,
111 geometry: Geometry,
112 properties: Map<String, Value>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 id: Option<Value>,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
119struct Geometry {
120 #[serde(rename = "type")]
121 type_: String,
122 coordinates: Vec<f64>,
123}
124
125impl Format for GeoJsonFormat {
126 fn import<R: Read>(&self, reader: R) -> Result<Narrative> {
127 let fc: FeatureCollection = serde_json::from_reader(reader)?;
128
129 if fc.type_ != "FeatureCollection" {
130 return Err(Error::InvalidFormat(
131 "expected GeoJSON FeatureCollection".to_string(),
132 ));
133 }
134
135 let mut builder = NarrativeBuilder::new();
136
137 if let Some(props) = fc.properties {
139 if let Some(title) = props.get("title").and_then(|v| v.as_str()) {
140 builder = builder.title(title);
141 }
142 if let Some(desc) = props.get("description").and_then(|v| v.as_str()) {
143 builder = builder.description(desc);
144 }
145 }
146
147 for feature in fc.features {
149 if feature.geometry.type_ != "Point" {
150 continue; }
152
153 let coords = &feature.geometry.coordinates;
154 if coords.len() < 2 {
155 continue; }
157
158 let lon = coords[0];
159 let lat = coords[1];
160 let mut location = Location::new(lat, lon);
161 if let Some(elev) = coords.get(2).copied() {
162 location.elevation = Some(elev);
163 }
164
165 let props = &feature.properties;
166
167 let timestamp = if let Some(ts_str) = props
169 .get(&self.options.timestamp_property)
170 .and_then(|v| v.as_str())
171 {
172 Timestamp::parse(ts_str)
173 .map_err(|e| Error::InvalidFormat(format!("invalid timestamp: {}", e)))?
174 } else {
175 Timestamp::now() };
177
178 let mut event_builder = EventBuilder::new().location(location).timestamp(timestamp);
180
181 if let Some(text) = props
183 .get(&self.options.text_property)
184 .and_then(|v| v.as_str())
185 {
186 event_builder = event_builder.text(text);
187 }
188
189 if let Some(tags) = props.get("tags").and_then(|v| v.as_array()) {
191 for tag in tags {
192 if let Some(tag_str) = tag.as_str() {
193 event_builder = event_builder.tag(tag_str);
194 }
195 }
196 }
197
198 if let Some(source_obj) = props.get("source").and_then(|v| v.as_object()) {
200 let source_type = source_obj
201 .get("type")
202 .and_then(|v| v.as_str())
203 .and_then(|s| match s.to_lowercase().as_str() {
204 "article" => Some(SourceType::Article),
205 "report" => Some(SourceType::Report),
206 "witness" => Some(SourceType::Witness),
207 "sensor" => Some(SourceType::Sensor),
208 _ => None,
209 })
210 .unwrap_or(SourceType::Article);
211
212 let mut source = SourceRef::new(source_type);
213 if let Some(url) = source_obj.get("url").and_then(|v| v.as_str()) {
214 source.url = Some(url.to_string());
215 }
216 if let Some(title) = source_obj.get("title").and_then(|v| v.as_str()) {
217 source.title = Some(title.to_string());
218 }
219 event_builder = event_builder.source(source);
220 }
221
222 let event = event_builder.build();
223 builder = builder.event(event);
224 }
225
226 Ok(builder.build())
227 }
228
229 fn export<W: Write>(&self, narrative: &Narrative, mut writer: W) -> Result<()> {
230 let mut features = Vec::new();
231
232 for event in narrative.events() {
233 let loc = &event.location;
234 let coords = if let Some(elev) = loc.elevation {
235 vec![loc.lon, loc.lat, elev]
236 } else {
237 vec![loc.lon, loc.lat]
238 };
239
240 let geometry = Geometry {
241 type_: "Point".to_string(),
242 coordinates: coords,
243 };
244
245 let mut properties = Map::new();
246
247 properties.insert(
249 self.options.timestamp_property.clone(),
250 Value::String(event.timestamp.to_rfc3339()),
251 );
252
253 properties.insert(
255 self.options.text_property.clone(),
256 Value::String(event.text.clone()),
257 );
258
259 if self.options.include_tags && !event.tags.is_empty() {
261 let tags: Vec<Value> = event
262 .tags
263 .iter()
264 .map(|t| Value::String(t.clone()))
265 .collect();
266 properties.insert("tags".to_string(), Value::Array(tags));
267 }
268
269 if self.options.include_sources && !event.sources.is_empty() {
271 let source = &event.sources[0]; let mut source_obj = Map::new();
273 source_obj.insert(
274 "type".to_string(),
275 Value::String(source.source_type.to_string()),
276 );
277 if let Some(url) = &source.url {
278 source_obj.insert("url".to_string(), Value::String(url.clone()));
279 }
280 if let Some(title) = &source.title {
281 source_obj.insert("title".to_string(), Value::String(title.clone()));
282 }
283 properties.insert("source".to_string(), Value::Object(source_obj));
284 }
285
286 let feature = Feature {
287 type_: "Feature".to_string(),
288 geometry,
289 properties,
290 id: if self.options.include_ids {
291 Some(Value::String(event.id.to_string()))
292 } else {
293 None
294 },
295 };
296
297 features.push(feature);
298 }
299
300 let mut fc_properties = Map::new();
302 fc_properties.insert("title".to_string(), Value::String(narrative.title.clone()));
303 if let Some(desc) = &narrative.metadata.description {
304 fc_properties.insert("description".to_string(), Value::String(desc.clone()));
305 }
306
307 let fc = FeatureCollection {
308 type_: "FeatureCollection".to_string(),
309 features,
310 properties: if fc_properties.is_empty() {
311 None
312 } else {
313 Some(fc_properties)
314 },
315 };
316
317 serde_json::to_writer_pretty(&mut writer, &fc)?;
318 Ok(())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::core::Event;
326
327 #[test]
328 fn test_geojson_import_basic() {
329 let geojson = r#"{
330 "type": "FeatureCollection",
331 "features": [
332 {
333 "type": "Feature",
334 "geometry": {
335 "type": "Point",
336 "coordinates": [-74.006, 40.7128]
337 },
338 "properties": {
339 "text": "Event at NYC",
340 "timestamp": "2024-01-15T14:30:00Z"
341 }
342 }
343 ]
344 }"#;
345
346 let format = GeoJsonFormat::new();
347 let narrative = format.import_str(geojson).unwrap();
348
349 assert_eq!(narrative.events().len(), 1);
350 let event = &narrative.events()[0];
351 assert_eq!(event.location.lat, 40.7128);
352 assert_eq!(event.location.lon, -74.006);
353 assert_eq!(event.text.as_str(), "Event at NYC");
354 }
355
356 #[test]
357 fn test_geojson_roundtrip() {
358 let event = Event::builder()
359 .location(Location::new(40.7128, -74.006))
360 .timestamp(Timestamp::parse("2024-01-15T14:30:00Z").unwrap())
361 .text("Test event")
362 .tag("test")
363 .build();
364
365 let narrative = Narrative::builder()
366 .title("Test Narrative")
367 .event(event)
368 .build();
369
370 let format = GeoJsonFormat::new();
371 let exported = format.export_str(&narrative).unwrap();
372 let imported = format.import_str(&exported).unwrap();
373
374 assert_eq!(imported.events().len(), 1);
375 assert_eq!(imported.title, "Test Narrative");
376 }
377
378 #[test]
379 fn test_geojson_with_elevation() {
380 let geojson = r#"{
381 "type": "FeatureCollection",
382 "features": [
383 {
384 "type": "Feature",
385 "geometry": {
386 "type": "Point",
387 "coordinates": [-122.4194, 37.7749, 100.5]
388 },
389 "properties": {
390 "timestamp": "2024-01-15T14:30:00Z"
391 }
392 }
393 ]
394 }"#;
395
396 let format = GeoJsonFormat::new();
397 let narrative = format.import_str(geojson).unwrap();
398
399 let event = &narrative.events()[0];
400 assert_eq!(event.location.elevation, Some(100.5));
401 }
402}