1use chrono::{Datelike, Duration, TimeZone};
4use serde::{Deserialize, Serialize};
5
6use crate::core::{Location, Timestamp};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
26pub struct GeoBounds {
27 pub min_lat: f64,
29 pub min_lon: f64,
31 pub max_lat: f64,
33 pub max_lon: f64,
35}
36
37impl GeoBounds {
38 pub fn new(min_lat: f64, min_lon: f64, max_lat: f64, max_lon: f64) -> Self {
47 Self {
48 min_lat,
49 min_lon,
50 max_lat,
51 max_lon,
52 }
53 }
54
55 pub fn from_corners(sw: &Location, ne: &Location) -> Self {
57 Self::new(sw.lat, sw.lon, ne.lat, ne.lon)
58 }
59
60 pub fn from_locations<'a>(locations: impl IntoIterator<Item = &'a Location>) -> Option<Self> {
62 let mut iter = locations.into_iter();
63 let first = iter.next()?;
64
65 let mut bounds = Self::new(first.lat, first.lon, first.lat, first.lon);
66
67 for loc in iter {
68 bounds.expand_to_include(loc);
69 }
70
71 Some(bounds)
72 }
73
74 pub fn from_center_degrees(center: &Location, lat_radius: f64, lon_radius: f64) -> Self {
76 Self::new(
77 center.lat - lat_radius,
78 center.lon - lon_radius,
79 center.lat + lat_radius,
80 center.lon + lon_radius,
81 )
82 }
83
84 pub fn contains(&self, location: &Location) -> bool {
86 location.lat >= self.min_lat
87 && location.lat <= self.max_lat
88 && location.lon >= self.min_lon
89 && location.lon <= self.max_lon
90 }
91
92 pub fn intersects(&self, other: &GeoBounds) -> bool {
94 self.min_lat <= other.max_lat
95 && self.max_lat >= other.min_lat
96 && self.min_lon <= other.max_lon
97 && self.max_lon >= other.min_lon
98 }
99
100 pub fn intersection(&self, other: &GeoBounds) -> Option<GeoBounds> {
102 if !self.intersects(other) {
103 return None;
104 }
105
106 Some(GeoBounds::new(
107 self.min_lat.max(other.min_lat),
108 self.min_lon.max(other.min_lon),
109 self.max_lat.min(other.max_lat),
110 self.max_lon.min(other.max_lon),
111 ))
112 }
113
114 pub fn union(&self, other: &GeoBounds) -> GeoBounds {
116 GeoBounds::new(
117 self.min_lat.min(other.min_lat),
118 self.min_lon.min(other.min_lon),
119 self.max_lat.max(other.max_lat),
120 self.max_lon.max(other.max_lon),
121 )
122 }
123
124 pub fn expand_to_include(&mut self, location: &Location) {
126 self.min_lat = self.min_lat.min(location.lat);
127 self.max_lat = self.max_lat.max(location.lat);
128 self.min_lon = self.min_lon.min(location.lon);
129 self.max_lon = self.max_lon.max(location.lon);
130 }
131
132 pub fn center(&self) -> Location {
134 Location::new(
135 (self.min_lat + self.max_lat) / 2.0,
136 (self.min_lon + self.max_lon) / 2.0,
137 )
138 }
139
140 pub fn width(&self) -> f64 {
142 self.max_lon - self.min_lon
143 }
144
145 pub fn height(&self) -> f64 {
147 self.max_lat - self.min_lat
148 }
149
150 pub fn southwest(&self) -> Location {
152 Location::new(self.min_lat, self.min_lon)
153 }
154
155 pub fn northeast(&self) -> Location {
157 Location::new(self.max_lat, self.max_lon)
158 }
159
160 pub fn northwest(&self) -> Location {
162 Location::new(self.max_lat, self.min_lon)
163 }
164
165 pub fn southeast(&self) -> Location {
167 Location::new(self.min_lat, self.max_lon)
168 }
169
170 pub fn to_geo_rect(&self) -> geo_types::Rect<f64> {
172 geo_types::Rect::new(
173 geo_types::coord! { x: self.min_lon, y: self.min_lat },
174 geo_types::coord! { x: self.max_lon, y: self.max_lat },
175 )
176 }
177}
178
179impl Default for GeoBounds {
180 fn default() -> Self {
181 Self::new(-90.0, -180.0, 90.0, 180.0)
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct TimeRange {
204 pub start: Timestamp,
206 pub end: Timestamp,
208}
209
210impl TimeRange {
211 pub fn new(start: Timestamp, end: Timestamp) -> Self {
213 Self { start, end }
214 }
215
216 pub fn year(year: i32) -> Self {
218 let start = Timestamp::parse(&format!("{}", year)).unwrap();
219 let end_dt = chrono::NaiveDate::from_ymd_opt(year, 12, 31)
220 .unwrap()
221 .and_hms_opt(23, 59, 59)
222 .unwrap();
223 let end = Timestamp::new(chrono::Utc.from_utc_datetime(&end_dt));
224 Self::new(start, end)
225 }
226
227 pub fn month(year: i32, month: u32) -> Self {
229 let start = Timestamp::parse(&format!("{}-{:02}", year, month)).unwrap();
230
231 let next_month = if month == 12 { 1 } else { month + 1 };
233 let next_year = if month == 12 { year + 1 } else { year };
234 let last_day = chrono::NaiveDate::from_ymd_opt(next_year, next_month, 1)
235 .unwrap()
236 .pred_opt()
237 .unwrap()
238 .day();
239
240 let end_dt = chrono::NaiveDate::from_ymd_opt(year, month, last_day)
241 .unwrap()
242 .and_hms_opt(23, 59, 59)
243 .unwrap();
244 let end = Timestamp::new(chrono::Utc.from_utc_datetime(&end_dt));
245
246 Self::new(start, end)
247 }
248
249 pub fn day(year: i32, month: u32, day: u32) -> Self {
251 let start = Timestamp::parse(&format!("{}-{:02}-{:02}", year, month, day)).unwrap();
252 let end_dt = chrono::NaiveDate::from_ymd_opt(year, month, day)
253 .unwrap()
254 .and_hms_opt(23, 59, 59)
255 .unwrap();
256 let end = Timestamp::new(chrono::Utc.from_utc_datetime(&end_dt));
257 Self::new(start, end)
258 }
259
260 pub fn last(duration: Duration) -> Self {
262 let end = Timestamp::now();
263 let start = Timestamp::new(end.datetime - duration);
264 Self::new(start, end)
265 }
266
267 pub fn next(duration: Duration) -> Self {
269 let start = Timestamp::now();
270 let end = Timestamp::new(start.datetime + duration);
271 Self::new(start, end)
272 }
273
274 pub fn contains(&self, timestamp: &Timestamp) -> bool {
276 timestamp >= &self.start && timestamp <= &self.end
277 }
278
279 pub fn overlaps(&self, other: &TimeRange) -> bool {
281 self.start <= other.end && self.end >= other.start
282 }
283
284 pub fn intersection(&self, other: &TimeRange) -> Option<TimeRange> {
286 if !self.overlaps(other) {
287 return None;
288 }
289
290 let start = if self.start > other.start {
291 self.start.clone()
292 } else {
293 other.start.clone()
294 };
295
296 let end = if self.end < other.end {
297 self.end.clone()
298 } else {
299 other.end.clone()
300 };
301
302 Some(TimeRange::new(start, end))
303 }
304
305 pub fn union(&self, other: &TimeRange) -> TimeRange {
307 let start = if self.start < other.start {
308 self.start.clone()
309 } else {
310 other.start.clone()
311 };
312
313 let end = if self.end > other.end {
314 self.end.clone()
315 } else {
316 other.end.clone()
317 };
318
319 TimeRange::new(start, end)
320 }
321
322 pub fn duration(&self) -> Duration {
324 self.end.duration_since(&self.start)
325 }
326
327 pub fn split(&self, chunk_duration: Duration) -> Vec<TimeRange> {
329 let mut ranges = Vec::new();
330 let mut current_start = self.start.clone();
331
332 while current_start < self.end {
333 let chunk_end = Timestamp::new(current_start.datetime + chunk_duration);
334 let actual_end = if chunk_end > self.end {
335 self.end.clone()
336 } else {
337 chunk_end
338 };
339
340 ranges.push(TimeRange::new(current_start.clone(), actual_end.clone()));
341 current_start = Timestamp::new(actual_end.datetime + Duration::seconds(1));
342 }
343
344 ranges
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_geobounds_contains() {
354 let bounds = GeoBounds::new(37.0, -123.0, 38.5, -121.5);
355
356 let inside = Location::new(37.7749, -122.4194);
357 let outside = Location::new(35.0, -120.0);
358
359 assert!(bounds.contains(&inside));
360 assert!(!bounds.contains(&outside));
361 }
362
363 #[test]
364 fn test_geobounds_intersects() {
365 let bounds1 = GeoBounds::new(0.0, 0.0, 10.0, 10.0);
366 let bounds2 = GeoBounds::new(5.0, 5.0, 15.0, 15.0);
367 let bounds3 = GeoBounds::new(20.0, 20.0, 30.0, 30.0);
368
369 assert!(bounds1.intersects(&bounds2));
370 assert!(!bounds1.intersects(&bounds3));
371 }
372
373 #[test]
374 fn test_geobounds_from_locations() {
375 let locations = vec![
376 Location::new(10.0, 20.0),
377 Location::new(30.0, 40.0),
378 Location::new(20.0, 30.0),
379 ];
380
381 let bounds = GeoBounds::from_locations(&locations).unwrap();
382 assert_eq!(bounds.min_lat, 10.0);
383 assert_eq!(bounds.max_lat, 30.0);
384 assert_eq!(bounds.min_lon, 20.0);
385 assert_eq!(bounds.max_lon, 40.0);
386 }
387
388 #[test]
389 fn test_geobounds_center() {
390 let bounds = GeoBounds::new(0.0, 0.0, 10.0, 10.0);
391 let center = bounds.center();
392 assert_eq!(center.lat, 5.0);
393 assert_eq!(center.lon, 5.0);
394 }
395
396 #[test]
397 fn test_timerange_year() {
398 let range = TimeRange::year(2024);
399 let inside = Timestamp::parse("2024-06-15T12:00:00Z").unwrap();
400 let outside = Timestamp::parse("2023-06-15T12:00:00Z").unwrap();
401
402 assert!(range.contains(&inside));
403 assert!(!range.contains(&outside));
404 }
405
406 #[test]
407 fn test_timerange_month() {
408 let range = TimeRange::month(2024, 3);
409
410 let inside = Timestamp::parse("2024-03-15T12:00:00Z").unwrap();
411 let outside = Timestamp::parse("2024-04-15T12:00:00Z").unwrap();
412
413 assert!(range.contains(&inside));
414 assert!(!range.contains(&outside));
415 }
416
417 #[test]
418 fn test_timerange_overlaps() {
419 let range1 = TimeRange::month(2024, 3);
420 let range2 = TimeRange::new(
421 Timestamp::parse("2024-03-15T00:00:00Z").unwrap(),
422 Timestamp::parse("2024-04-15T00:00:00Z").unwrap(),
423 );
424 let range3 = TimeRange::month(2024, 5);
425
426 assert!(range1.overlaps(&range2));
427 assert!(!range1.overlaps(&range3));
428 }
429
430 #[test]
431 fn test_timerange_duration() {
432 let range = TimeRange::day(2024, 3, 15);
433 let duration = range.duration();
434 assert!(duration.num_hours() >= 23);
436 }
437}