Skip to main content

spatial_narrative/core/
traits.rs

1//! Core traits for spatial and temporal entities.
2
3use crate::core::{GeoBounds, Location, TimeRange, Timestamp};
4
5/// Trait for entities with a spatial location.
6///
7/// This trait provides a common interface for anything that has
8/// a geographic location, enabling spatial indexing and queries.
9pub trait SpatialEntity {
10    /// Returns the primary location of this entity.
11    fn location(&self) -> &Location;
12
13    /// Returns the geographic bounds of this entity.
14    ///
15    /// For point entities, this returns a zero-size bounding box
16    /// at the location. For entities with extent, this returns
17    /// the full bounding box.
18    fn bounds(&self) -> GeoBounds {
19        let loc = self.location();
20        GeoBounds::new(loc.lat, loc.lon, loc.lat, loc.lon)
21    }
22
23    /// Returns the location as a geo-types Point.
24    fn to_geo_point(&self) -> geo_types::Point<f64> {
25        self.location().to_geo_point()
26    }
27
28    /// Checks if this entity is within the given bounds.
29    fn is_within_bounds(&self, bounds: &GeoBounds) -> bool {
30        bounds.contains(self.location())
31    }
32}
33
34/// Trait for entities with temporal information.
35///
36/// This trait provides a common interface for anything that has
37/// a timestamp, enabling temporal indexing and queries.
38pub trait TemporalEntity {
39    /// Returns the primary timestamp of this entity.
40    fn timestamp(&self) -> &Timestamp;
41
42    /// Returns the time range of this entity.
43    ///
44    /// For instantaneous events, this returns a range where
45    /// start equals end. For entities with duration, this
46    /// returns the full time span.
47    fn time_range(&self) -> TimeRange {
48        let ts = self.timestamp();
49        TimeRange::new(ts.clone(), ts.clone())
50    }
51
52    /// Checks if this entity falls within the given time range.
53    fn is_within_time_range(&self, range: &TimeRange) -> bool {
54        range.contains(self.timestamp())
55    }
56}
57
58/// Trait for entities that can be spatiotemporally indexed.
59///
60/// This is a convenience trait that combines `SpatialEntity` and
61/// `TemporalEntity` for entities that have both space and time.
62#[allow(dead_code)]
63pub trait SpatiotemporalEntity: SpatialEntity + TemporalEntity {
64    /// Checks if this entity is within both spatial bounds and time range.
65    fn is_within(&self, bounds: &GeoBounds, range: &TimeRange) -> bool {
66        self.is_within_bounds(bounds) && self.is_within_time_range(range)
67    }
68}
69
70// Blanket implementation for anything that implements both traits
71impl<T: SpatialEntity + TemporalEntity> SpatiotemporalEntity for T {}
72
73// Implement traits for Event
74use crate::core::Event;
75
76impl SpatialEntity for Event {
77    fn location(&self) -> &Location {
78        &self.location
79    }
80}
81
82impl TemporalEntity for Event {
83    fn timestamp(&self) -> &Timestamp {
84        &self.timestamp
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_event_spatial_entity() {
94        let event = Event::builder()
95            .location(Location::new(40.7128, -74.0060))
96            .timestamp(Timestamp::now())
97            .text("Test")
98            .build();
99
100        assert_eq!(event.location().lat, 40.7128);
101        assert_eq!(event.location().lon, -74.0060);
102    }
103
104    #[test]
105    fn test_event_is_within_bounds() {
106        let event = Event::builder()
107            .location(Location::new(40.7128, -74.0060))
108            .timestamp(Timestamp::now())
109            .text("NYC Event")
110            .build();
111
112        let nyc_bounds = GeoBounds::new(40.0, -75.0, 41.0, -73.0);
113        let la_bounds = GeoBounds::new(33.0, -119.0, 35.0, -117.0);
114
115        assert!(event.is_within_bounds(&nyc_bounds));
116        assert!(!event.is_within_bounds(&la_bounds));
117    }
118
119    #[test]
120    fn test_event_temporal_entity() {
121        let event = Event::builder()
122            .location(Location::new(40.0, -74.0))
123            .timestamp(Timestamp::parse("2024-03-15T12:00:00Z").unwrap())
124            .text("Test")
125            .build();
126
127        let march = TimeRange::month(2024, 3);
128        let april = TimeRange::month(2024, 4);
129
130        assert!(event.is_within_time_range(&march));
131        assert!(!event.is_within_time_range(&april));
132    }
133
134    #[test]
135    fn test_event_spatiotemporal() {
136        let event = Event::builder()
137            .location(Location::new(40.7128, -74.0060))
138            .timestamp(Timestamp::parse("2024-03-15T12:00:00Z").unwrap())
139            .text("NYC in March")
140            .build();
141
142        let nyc_bounds = GeoBounds::new(40.0, -75.0, 41.0, -73.0);
143        let march = TimeRange::month(2024, 3);
144        let april = TimeRange::month(2024, 4);
145
146        assert!(event.is_within(&nyc_bounds, &march));
147        assert!(!event.is_within(&nyc_bounds, &april));
148    }
149}