1use super::{SpatialIndex, TemporalIndex};
30use crate::core::{GeoBounds, Location, TimeRange, Timestamp};
31
32#[derive(Debug)]
37pub struct SpatiotemporalIndex<T> {
38 spatial: SpatialIndex<usize>,
39 temporal: TemporalIndex<usize>,
40 items: Vec<T>,
41 locations: Vec<Location>,
42 timestamps: Vec<Timestamp>,
43}
44
45impl<T: Clone> SpatiotemporalIndex<T> {
46 pub fn new() -> Self {
48 Self {
49 spatial: SpatialIndex::new(),
50 temporal: TemporalIndex::new(),
51 items: Vec::new(),
52 locations: Vec::new(),
53 timestamps: Vec::new(),
54 }
55 }
56
57 pub fn from_iter<I, L, Ts>(iter: I, location_fn: L, timestamp_fn: Ts) -> Self
59 where
60 I: IntoIterator<Item = T>,
61 L: Fn(&T) -> &Location,
62 Ts: Fn(&T) -> &Timestamp,
63 {
64 let mut index = Self::new();
65 for item in iter {
66 let loc = location_fn(&item).clone();
67 let ts = timestamp_fn(&item).clone();
68 index.insert(item, &loc, &ts);
69 }
70 index
71 }
72
73 pub fn insert(&mut self, item: T, location: &Location, timestamp: &Timestamp) {
75 let idx = self.items.len();
76
77 self.items.push(item);
78 self.locations.push(location.clone());
79 self.timestamps.push(timestamp.clone());
80
81 self.spatial.insert(idx, location);
82 self.temporal.insert(idx, timestamp);
83 }
84
85 pub fn query(&self, bounds: &GeoBounds, range: &TimeRange) -> Vec<&T> {
87 let spatial_indices: std::collections::HashSet<usize> = self
89 .spatial
90 .query_bounds(bounds)
91 .into_iter()
92 .copied()
93 .collect();
94
95 let temporal_indices: std::collections::HashSet<usize> = self
97 .temporal
98 .query_range(range)
99 .into_iter()
100 .copied()
101 .collect();
102
103 spatial_indices
105 .intersection(&temporal_indices)
106 .map(|&i| &self.items[i])
107 .collect()
108 }
109
110 pub fn query_spatial(&self, bounds: &GeoBounds) -> Vec<&T> {
112 self.spatial
113 .query_bounds(bounds)
114 .into_iter()
115 .map(|&i| &self.items[i])
116 .collect()
117 }
118
119 pub fn query_temporal(&self, range: &TimeRange) -> Vec<&T> {
121 self.temporal
122 .query_range(range)
123 .into_iter()
124 .map(|&i| &self.items[i])
125 .collect()
126 }
127
128 pub fn nearest_in_range(&self, lat: f64, lon: f64, k: usize, range: &TimeRange) -> Vec<&T> {
130 let temporal_indices: std::collections::HashSet<usize> = self
132 .temporal
133 .query_range(range)
134 .into_iter()
135 .copied()
136 .collect();
137
138 self.spatial
140 .nearest(lat, lon, k * 2) .into_iter()
142 .filter(|&i| temporal_indices.contains(i))
143 .take(k)
144 .map(|&i| &self.items[i])
145 .collect()
146 }
147
148 pub fn len(&self) -> usize {
150 self.items.len()
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.items.is_empty()
156 }
157
158 pub fn items(&self) -> &[T] {
160 &self.items
161 }
162
163 pub fn bounds(&self) -> Option<GeoBounds> {
165 if self.locations.is_empty() {
166 return None;
167 }
168 GeoBounds::from_locations(&self.locations)
169 }
170
171 pub fn time_range(&self) -> Option<TimeRange> {
173 self.temporal.time_range()
174 }
175}
176
177impl<T: Clone> Default for SpatiotemporalIndex<T> {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183#[derive(Debug, Clone)]
185pub struct GridSpec {
186 pub lat_cells: usize,
188 pub lon_cells: usize,
190 pub bounds: GeoBounds,
192}
193
194impl GridSpec {
195 pub fn new(bounds: GeoBounds, lat_cells: usize, lon_cells: usize) -> Self {
197 Self {
198 bounds,
199 lat_cells,
200 lon_cells,
201 }
202 }
203
204 pub fn square_cells(bounds: GeoBounds, target_cells: usize) -> Self {
206 let lat_range = bounds.max_lat - bounds.min_lat;
207 let lon_range = bounds.max_lon - bounds.min_lon;
208 let aspect = lon_range / lat_range;
209
210 let lat_cells = ((target_cells as f64 / aspect).sqrt() as usize).max(1);
211 let lon_cells = ((target_cells as f64 * aspect).sqrt() as usize).max(1);
212
213 Self {
214 bounds,
215 lat_cells,
216 lon_cells,
217 }
218 }
219
220 pub fn cell_size(&self) -> (f64, f64) {
222 let lat_size = (self.bounds.max_lat - self.bounds.min_lat) / self.lat_cells as f64;
223 let lon_size = (self.bounds.max_lon - self.bounds.min_lon) / self.lon_cells as f64;
224 (lat_size, lon_size)
225 }
226}
227
228#[derive(Debug, Clone)]
230pub struct Heatmap {
231 pub grid: GridSpec,
233 pub counts: Vec<usize>,
235 pub max_count: usize,
237}
238
239impl Heatmap {
240 pub fn get(&self, lat_idx: usize, lon_idx: usize) -> usize {
242 if lat_idx >= self.grid.lat_cells || lon_idx >= self.grid.lon_cells {
243 return 0;
244 }
245 self.counts[lat_idx * self.grid.lon_cells + lon_idx]
246 }
247
248 pub fn get_normalized(&self, lat_idx: usize, lon_idx: usize) -> f64 {
250 if self.max_count == 0 {
251 return 0.0;
252 }
253 self.get(lat_idx, lon_idx) as f64 / self.max_count as f64
254 }
255
256 pub fn to_grid(&self) -> Vec<Vec<usize>> {
258 self.counts
259 .chunks(self.grid.lon_cells)
260 .map(|row| row.to_vec())
261 .collect()
262 }
263}
264
265impl<T: Clone> SpatiotemporalIndex<T> {
266 pub fn heatmap(&self, grid: GridSpec) -> Heatmap {
268 let mut counts = vec![0usize; grid.lat_cells * grid.lon_cells];
269 let (lat_size, lon_size) = grid.cell_size();
270
271 for loc in &self.locations {
272 if !grid.bounds.contains(loc) {
273 continue;
274 }
275
276 let lat_idx = ((loc.lat - grid.bounds.min_lat) / lat_size) as usize;
277 let lon_idx = ((loc.lon - grid.bounds.min_lon) / lon_size) as usize;
278
279 let lat_idx = lat_idx.min(grid.lat_cells - 1);
280 let lon_idx = lon_idx.min(grid.lon_cells - 1);
281
282 counts[lat_idx * grid.lon_cells + lon_idx] += 1;
283 }
284
285 let max_count = counts.iter().copied().max().unwrap_or(0);
286
287 Heatmap {
288 grid,
289 counts,
290 max_count,
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn make_timestamp(day: u32) -> Timestamp {
300 Timestamp::parse(&format!("2024-01-{:02}T12:00:00Z", day)).unwrap()
301 }
302
303 #[test]
304 fn test_spatiotemporal_index_new() {
305 let index: SpatiotemporalIndex<String> = SpatiotemporalIndex::new();
306 assert!(index.is_empty());
307 }
308
309 #[test]
310 fn test_spatiotemporal_index_insert() {
311 let mut index = SpatiotemporalIndex::new();
312 index.insert("NYC", &Location::new(40.7128, -74.0060), &make_timestamp(1));
313 index.insert("LA", &Location::new(34.0522, -118.2437), &make_timestamp(5));
314
315 assert_eq!(index.len(), 2);
316 }
317
318 #[test]
319 fn test_spatiotemporal_query() {
320 let mut index = SpatiotemporalIndex::new();
321 index.insert(
322 "NYC Jan 1",
323 &Location::new(40.7128, -74.0060),
324 &make_timestamp(1),
325 );
326 index.insert(
327 "NYC Jan 15",
328 &Location::new(40.7128, -74.0060),
329 &make_timestamp(15),
330 );
331 index.insert(
332 "LA Jan 1",
333 &Location::new(34.0522, -118.2437),
334 &make_timestamp(1),
335 );
336 index.insert(
337 "LA Jan 15",
338 &Location::new(34.0522, -118.2437),
339 &make_timestamp(15),
340 );
341
342 let bounds = GeoBounds::new(35.0, -80.0, 45.0, -70.0);
344 let range = TimeRange::new(make_timestamp(1), make_timestamp(7));
345
346 let results = index.query(&bounds, &range);
347 assert_eq!(results.len(), 1);
348 assert_eq!(*results[0], "NYC Jan 1");
349 }
350
351 #[test]
352 fn test_heatmap_generation() {
353 let mut index: SpatiotemporalIndex<&str> = SpatiotemporalIndex::new();
354
355 for i in 0..10 {
357 index.insert(
358 "NYC area",
359 &Location::new(40.7 + (i as f64 * 0.01), -74.0 + (i as f64 * 0.01)),
360 &make_timestamp(1),
361 );
362 }
363 for i in 0..5 {
364 index.insert(
365 "LA area",
366 &Location::new(34.0 + (i as f64 * 0.01), -118.2 + (i as f64 * 0.01)),
367 &make_timestamp(1),
368 );
369 }
370
371 let bounds = GeoBounds::new(30.0, -125.0, 45.0, -70.0);
372 let grid = GridSpec::new(bounds, 10, 10);
373 let heatmap = index.heatmap(grid);
374
375 assert!(heatmap.max_count > 0);
376 }
377}