1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5pub struct GeoPoint {
6 pub lat: f64,
8 pub lon: f64,
10}
11
12impl GeoPoint {
13 pub fn new(lat: f64, lon: f64) -> Self {
15 Self { lat, lon }
16 }
17
18 pub fn distance_km(&self, other: &Self) -> f64 {
20 const EARTH_RADIUS_KM: f64 = 6371.0;
21
22 let d_lat = (other.lat - self.lat).to_radians();
23 let d_lon = (other.lon - self.lon).to_radians();
24 let lat1 = self.lat.to_radians();
25 let lat2 = other.lat.to_radians();
26
27 let a = (d_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (d_lon / 2.0).sin().powi(2);
28 let c = 2.0 * a.sqrt().asin();
29 EARTH_RADIUS_KM * c
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
35pub struct Bbox {
36 pub sw: GeoPoint,
38 pub ne: GeoPoint,
40}
41
42impl Bbox {
43 pub fn new(sw: GeoPoint, ne: GeoPoint) -> Self {
45 Self { sw, ne }
46 }
47
48 pub fn contains(&self, point: &GeoPoint) -> bool {
50 point.lat >= self.sw.lat
51 && point.lat <= self.ne.lat
52 && point.lon >= self.sw.lon
53 && point.lon <= self.ne.lon
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct GeoHotspot {
60 pub center: GeoPoint,
62 pub radius_km: f32,
64 pub intensity: f32,
66 pub event_count: u32,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn geopoint_distance_same_point_is_zero() {
76 let p = GeoPoint::new(4.711, -74.072);
77 assert!((p.distance_km(&p) - 0.0).abs() < 1e-9);
78 }
79
80 #[test]
81 fn geopoint_distance_bogota_to_cali() {
82 let bogota = GeoPoint::new(4.711, -74.072);
83 let cali = GeoPoint::new(3.4516, -76.532);
84 let km = bogota.distance_km(&cali);
85 assert!(km > 280.0 && km < 320.0, "distance was {km} km");
87 }
88
89 #[test]
90 fn bbox_contains_inside_point() {
91 let bbox = Bbox::new(GeoPoint::new(0.0, 0.0), GeoPoint::new(10.0, 10.0));
92 assert!(bbox.contains(&GeoPoint::new(5.0, 5.0)));
93 }
94
95 #[test]
96 fn bbox_does_not_contain_outside_point() {
97 let bbox = Bbox::new(GeoPoint::new(0.0, 0.0), GeoPoint::new(10.0, 10.0));
98 assert!(!bbox.contains(&GeoPoint::new(11.0, 5.0)));
99 }
100}