Skip to main content

spatial_narrative/core/
narrative.rs

1//! Narrative - a collection of related events forming a coherent story.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use crate::core::{Event, EventId, GeoBounds, TimeRange, Timestamp};
8use crate::error::{Error, Result};
9
10/// Unique identifier for a narrative.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct NarrativeId(pub Uuid);
14
15impl NarrativeId {
16    /// Creates a new random NarrativeId.
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20
21    /// Creates a NarrativeId from a UUID.
22    pub fn from_uuid(uuid: Uuid) -> Self {
23        Self(uuid)
24    }
25
26    /// Parses a NarrativeId from a string.
27    pub fn parse(s: &str) -> Result<Self> {
28        Uuid::parse_str(s)
29            .map(Self)
30            .map_err(|_| Error::ParseError(format!("invalid narrative ID: {}", s)))
31    }
32
33    /// Returns the inner UUID.
34    pub fn as_uuid(&self) -> &Uuid {
35        &self.0
36    }
37}
38
39impl Default for NarrativeId {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl std::fmt::Display for NarrativeId {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}", self.0)
48    }
49}
50
51/// Metadata associated with a narrative.
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
53pub struct NarrativeMetadata {
54    /// When the narrative was created.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub created: Option<Timestamp>,
57    /// When the narrative was last modified.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub modified: Option<Timestamp>,
60    /// Author or creator of the narrative.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub author: Option<String>,
63    /// Description of the narrative.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub description: Option<String>,
66    /// Category or type.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub category: Option<String>,
69    /// Additional key-value metadata.
70    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
71    pub extra: HashMap<String, String>,
72}
73
74impl NarrativeMetadata {
75    /// Creates empty metadata.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Creates metadata with creation timestamp set to now.
81    pub fn with_created_now() -> Self {
82        Self {
83            created: Some(Timestamp::now()),
84            ..Default::default()
85        }
86    }
87}
88
89/// A collection of related events forming a coherent story.
90///
91/// Narratives are the primary container for organizing events
92/// into meaningful stories with geographic and temporal structure.
93///
94/// # Examples
95///
96/// ```
97/// use spatial_narrative::core::{Narrative, Event, Location, Timestamp};
98///
99/// let mut narrative = Narrative::builder()
100///     .title("Hurricane Timeline")
101///     .description("Tracking the hurricane's path")
102///     .category("disaster")
103///     .build();
104///
105/// narrative.add_event(Event::builder()
106///     .location(Location::new(25.0, -80.0))
107///     .timestamp(Timestamp::now())
108///     .text("Hurricane makes landfall")
109///     .tag("landfall")
110///     .build());
111/// ```
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct Narrative {
114    /// Unique identifier.
115    pub id: NarrativeId,
116    /// Title of the narrative.
117    pub title: String,
118    /// Events in this narrative.
119    #[serde(default)]
120    pub events: Vec<Event>,
121    /// Narrative metadata.
122    #[serde(default)]
123    pub metadata: NarrativeMetadata,
124    /// Categorical tags for the narrative.
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub tags: Vec<String>,
127}
128
129impl Narrative {
130    /// Creates a new narrative with the given title.
131    pub fn new(title: impl Into<String>) -> Self {
132        Self {
133            id: NarrativeId::new(),
134            title: title.into(),
135            events: Vec::new(),
136            metadata: NarrativeMetadata::with_created_now(),
137            tags: Vec::new(),
138        }
139    }
140
141    /// Creates a builder for constructing a Narrative.
142    pub fn builder() -> NarrativeBuilder {
143        NarrativeBuilder::new()
144    }
145
146    /// Returns the events in this narrative.
147    pub fn events(&self) -> &[Event] {
148        &self.events
149    }
150
151    /// Returns a mutable reference to the events.
152    pub fn events_mut(&mut self) -> &mut Vec<Event> {
153        &mut self.events
154    }
155
156    /// Returns the number of events.
157    pub fn len(&self) -> usize {
158        self.events.len()
159    }
160
161    /// Returns true if the narrative has no events.
162    pub fn is_empty(&self) -> bool {
163        self.events.is_empty()
164    }
165
166    /// Adds an event to the narrative.
167    pub fn add_event(&mut self, event: Event) {
168        self.events.push(event);
169        self.metadata.modified = Some(Timestamp::now());
170    }
171
172    /// Removes an event by ID.
173    pub fn remove_event(&mut self, id: &EventId) -> Option<Event> {
174        if let Some(pos) = self.events.iter().position(|e| &e.id == id) {
175            self.metadata.modified = Some(Timestamp::now());
176            Some(self.events.remove(pos))
177        } else {
178            None
179        }
180    }
181
182    /// Finds an event by ID.
183    pub fn get_event(&self, id: &EventId) -> Option<&Event> {
184        self.events.iter().find(|e| &e.id == id)
185    }
186
187    /// Finds an event by ID (mutable).
188    pub fn get_event_mut(&mut self, id: &EventId) -> Option<&mut Event> {
189        self.events.iter_mut().find(|e| &e.id == id)
190    }
191
192    /// Returns events sorted by timestamp.
193    pub fn events_chronological(&self) -> Vec<&Event> {
194        let mut events: Vec<_> = self.events.iter().collect();
195        events.sort_by_key(|e| &e.timestamp);
196        events
197    }
198
199    /// Filters events by spatial bounds.
200    pub fn filter_spatial(&self, bounds: &GeoBounds) -> Vec<&Event> {
201        self.events
202            .iter()
203            .filter(|e| bounds.contains(&e.location))
204            .collect()
205    }
206
207    /// Filters events by time range.
208    pub fn filter_temporal(&self, range: &TimeRange) -> Vec<&Event> {
209        self.events
210            .iter()
211            .filter(|e| range.contains(&e.timestamp))
212            .collect()
213    }
214
215    /// Filters events by tag.
216    pub fn filter_by_tag(&self, tag: &str) -> Vec<&Event> {
217        self.events.iter().filter(|e| e.has_tag(tag)).collect()
218    }
219
220    /// Returns the geographic bounds of all events.
221    pub fn bounds(&self) -> Option<GeoBounds> {
222        let locations: Vec<_> = self.events.iter().map(|e| &e.location).collect();
223        GeoBounds::from_locations(locations)
224    }
225
226    /// Returns the time range spanning all events.
227    pub fn time_range(&self) -> Option<TimeRange> {
228        if self.events.is_empty() {
229            return None;
230        }
231
232        let mut min_ts = &self.events[0].timestamp;
233        let mut max_ts = &self.events[0].timestamp;
234
235        for event in &self.events {
236            if event.timestamp < *min_ts {
237                min_ts = &event.timestamp;
238            }
239            if event.timestamp > *max_ts {
240                max_ts = &event.timestamp;
241            }
242        }
243
244        Some(TimeRange::new(min_ts.clone(), max_ts.clone()))
245    }
246
247    /// Returns all unique tags used by events in this narrative.
248    pub fn all_tags(&self) -> Vec<&str> {
249        let mut tags: Vec<_> = self
250            .events
251            .iter()
252            .flat_map(|e| e.tags.iter().map(|s| s.as_str()))
253            .collect();
254        tags.sort();
255        tags.dedup();
256        tags
257    }
258
259    /// Adds a tag to the narrative.
260    pub fn add_tag(&mut self, tag: impl Into<String>) {
261        let tag = tag.into();
262        if !self.tags.contains(&tag) {
263            self.tags.push(tag);
264        }
265    }
266
267    /// Creates a new narrative containing only events that match the predicate.
268    pub fn filter<F>(&self, predicate: F) -> Narrative
269    where
270        F: Fn(&Event) -> bool,
271    {
272        let events = self
273            .events
274            .iter()
275            .filter(|e| predicate(e))
276            .cloned()
277            .collect();
278        Narrative {
279            id: NarrativeId::new(),
280            title: format!("{} (filtered)", self.title),
281            events,
282            metadata: NarrativeMetadata::with_created_now(),
283            tags: self.tags.clone(),
284        }
285    }
286
287    /// Merges another narrative into this one.
288    pub fn merge(&mut self, other: Narrative) {
289        self.events.extend(other.events);
290        self.metadata.modified = Some(Timestamp::now());
291    }
292}
293
294impl Default for Narrative {
295    fn default() -> Self {
296        Self::new("Untitled")
297    }
298}
299
300/// Builder for constructing [`Narrative`] instances.
301#[derive(Debug, Default)]
302pub struct NarrativeBuilder {
303    id: Option<NarrativeId>,
304    title: Option<String>,
305    events: Vec<Event>,
306    metadata: NarrativeMetadata,
307    tags: Vec<String>,
308}
309
310impl NarrativeBuilder {
311    /// Creates a new NarrativeBuilder.
312    pub fn new() -> Self {
313        Self {
314            metadata: NarrativeMetadata::with_created_now(),
315            ..Default::default()
316        }
317    }
318
319    /// Sets the narrative ID.
320    pub fn id(mut self, id: NarrativeId) -> Self {
321        self.id = Some(id);
322        self
323    }
324
325    /// Sets the title.
326    pub fn title(mut self, title: impl Into<String>) -> Self {
327        self.title = Some(title.into());
328        self
329    }
330
331    /// Adds an event.
332    pub fn event(mut self, event: Event) -> Self {
333        self.events.push(event);
334        self
335    }
336
337    /// Adds multiple events.
338    pub fn events(mut self, events: impl IntoIterator<Item = Event>) -> Self {
339        self.events.extend(events);
340        self
341    }
342
343    /// Sets the author.
344    pub fn author(mut self, author: impl Into<String>) -> Self {
345        self.metadata.author = Some(author.into());
346        self
347    }
348
349    /// Sets the description.
350    pub fn description(mut self, description: impl Into<String>) -> Self {
351        self.metadata.description = Some(description.into());
352        self
353    }
354
355    /// Sets the category.
356    pub fn category(mut self, category: impl Into<String>) -> Self {
357        self.metadata.category = Some(category.into());
358        self
359    }
360
361    /// Adds a metadata key-value pair.
362    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
363        self.metadata.extra.insert(key.into(), value.into());
364        self
365    }
366
367    /// Adds a tag.
368    pub fn tag(mut self, tag: impl Into<String>) -> Self {
369        self.tags.push(tag.into());
370        self
371    }
372
373    /// Adds multiple tags.
374    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
375        self.tags.extend(tags.into_iter().map(Into::into));
376        self
377    }
378
379    /// Builds the Narrative.
380    pub fn build(self) -> Narrative {
381        Narrative {
382            id: self.id.unwrap_or_default(),
383            title: self.title.unwrap_or_else(|| "Untitled".to_string()),
384            events: self.events,
385            metadata: self.metadata,
386            tags: self.tags,
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::core::Location;
395
396    fn make_event(lat: f64, lon: f64, time: &str, text: &str) -> Event {
397        Event::builder()
398            .location(Location::new(lat, lon))
399            .timestamp(Timestamp::parse(time).unwrap())
400            .text(text)
401            .build()
402    }
403
404    #[test]
405    fn test_narrative_new() {
406        let narrative = Narrative::new("Test Narrative");
407        assert_eq!(narrative.title, "Test Narrative");
408        assert!(narrative.is_empty());
409    }
410
411    #[test]
412    fn test_narrative_builder() {
413        let narrative = Narrative::builder()
414            .title("Hurricane Timeline")
415            .description("Tracking the storm")
416            .author("Weather Service")
417            .category("disaster")
418            .tag("weather")
419            .build();
420
421        assert_eq!(narrative.title, "Hurricane Timeline");
422        assert_eq!(
423            narrative.metadata.description,
424            Some("Tracking the storm".to_string())
425        );
426        assert!(narrative.tags.contains(&"weather".to_string()));
427    }
428
429    #[test]
430    fn test_narrative_add_events() {
431        let mut narrative = Narrative::new("Test");
432
433        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Event 1"));
434        narrative.add_event(make_event(41.0, -73.0, "2024-03-15T11:00:00Z", "Event 2"));
435
436        assert_eq!(narrative.len(), 2);
437    }
438
439    #[test]
440    fn test_narrative_filter_spatial() {
441        let mut narrative = Narrative::new("Test");
442        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "NYC"));
443        narrative.add_event(make_event(34.0, -118.0, "2024-03-15T11:00:00Z", "LA"));
444
445        let nyc_bounds = GeoBounds::new(39.0, -75.0, 41.0, -73.0);
446        let filtered = narrative.filter_spatial(&nyc_bounds);
447
448        assert_eq!(filtered.len(), 1);
449        assert_eq!(filtered[0].text, "NYC");
450    }
451
452    #[test]
453    fn test_narrative_filter_temporal() {
454        let mut narrative = Narrative::new("Test");
455        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "March"));
456        narrative.add_event(make_event(40.0, -74.0, "2024-04-15T10:00:00Z", "April"));
457
458        let march = TimeRange::month(2024, 3);
459        let filtered = narrative.filter_temporal(&march);
460
461        assert_eq!(filtered.len(), 1);
462        assert_eq!(filtered[0].text, "March");
463    }
464
465    #[test]
466    fn test_narrative_bounds() {
467        let mut narrative = Narrative::new("Test");
468        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "NYC"));
469        narrative.add_event(make_event(34.0, -118.0, "2024-03-15T11:00:00Z", "LA"));
470
471        let bounds = narrative.bounds().unwrap();
472        assert_eq!(bounds.min_lat, 34.0);
473        assert_eq!(bounds.max_lat, 40.0);
474        assert_eq!(bounds.min_lon, -118.0);
475        assert_eq!(bounds.max_lon, -74.0);
476    }
477
478    #[test]
479    fn test_narrative_time_range() {
480        let mut narrative = Narrative::new("Test");
481        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "First"));
482        narrative.add_event(make_event(40.0, -74.0, "2024-03-20T10:00:00Z", "Last"));
483
484        let range = narrative.time_range().unwrap();
485        let duration = range.duration();
486        assert_eq!(duration.num_days(), 5);
487    }
488
489    #[test]
490    fn test_narrative_events_chronological() {
491        let mut narrative = Narrative::new("Test");
492        narrative.add_event(make_event(40.0, -74.0, "2024-03-20T10:00:00Z", "Third"));
493        narrative.add_event(make_event(40.0, -74.0, "2024-03-10T10:00:00Z", "First"));
494        narrative.add_event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Second"));
495
496        let sorted = narrative.events_chronological();
497        assert_eq!(sorted[0].text, "First");
498        assert_eq!(sorted[1].text, "Second");
499        assert_eq!(sorted[2].text, "Third");
500    }
501
502    #[test]
503    fn test_narrative_serialization() {
504        let narrative = Narrative::builder()
505            .title("Test")
506            .event(make_event(40.0, -74.0, "2024-03-15T10:00:00Z", "Event"))
507            .build();
508
509        let json = serde_json::to_string(&narrative).unwrap();
510        let parsed: Narrative = serde_json::from_str(&json).unwrap();
511
512        assert_eq!(narrative.title, parsed.title);
513        assert_eq!(narrative.events.len(), parsed.events.len());
514    }
515}