1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use tokio::sync::RwLock;
6use tracing::instrument;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct UserLocation {
10 pub lat: f64,
11 pub lng: f64,
12 pub accuracy: f64,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ContextZone {
17 pub name: String,
18 pub lat: f64,
19 pub lng: f64,
20 pub radius_m: f64,
21}
22
23#[derive(Clone)]
24pub struct GeoService {
25 pub locations: Arc<RwLock<HashMap<i32, UserLocation>>>,
26 pub zones: Arc<RwLock<HashMap<String, ContextZone>>>,
27}
28
29impl GeoService {
30 pub fn new() -> Self {
31 let zones = std::env::var("GEO_ZONES").ok().and_then(|json| {
32 serde_json::from_str::<Vec<ContextZone>>(&json)
33 .map_err(|e| tracing::warn!("GEO_ZONES parse error: {e}"))
34 .ok()
35 }).unwrap_or_default();
36
37 let zone_map = zones.into_iter().map(|z| (z.name.clone(), z)).collect();
38
39 Self {
40 locations: Arc::new(RwLock::new(HashMap::new())),
41 zones: Arc::new(RwLock::new(zone_map)),
42 }
43 }
44
45 #[instrument(skip(self))]
46 pub async fn update_location(&self, user_id: i32, loc: UserLocation) {
47 self.locations.write().await.insert(user_id, loc);
48 tracing::info!(user_id, "location updated");
49 }
50
51 #[instrument(skip(self))]
52 pub async fn add_zone(&self, zone: ContextZone) {
53 tracing::info!(zone_name = %zone.name, "zone added");
54 self.zones.write().await.insert(zone.name.clone(), zone);
55 }
56
57 #[allow(dead_code)]
58 pub async fn check_proximity(&self, user_id: i32) -> Vec<String> {
59 let location = self.locations.read().await.get(&user_id).cloned();
60 let Some(loc) = location else {
61 return Vec::new();
62 };
63 let zones = self.zones.read().await.clone();
64 zones
65 .into_values()
66 .filter(|z| {
67 let d = haversine(loc.lat, loc.lng, z.lat, z.lng);
68 d <= z.radius_m
69 })
70 .map(|z| z.name)
71 .collect()
72 }
73}
74
75#[allow(dead_code, clippy::suboptimal_flops)]
76fn haversine(lat1: f64, lng1: f64, lat2: f64, lng2: f64) -> f64 {
77 let r = 6_371_000_f64;
78 let d_lat = (lat2 - lat1).to_radians();
79 let d_lng = (lng2 - lng1).to_radians();
80 let a = (d_lat / 2_f64).sin().powi(2)
81 + lat1.to_radians().cos() * lat2.to_radians().cos() * (d_lng / 2_f64).sin().powi(2);
82 r * 2_f64 * a.sqrt().asin()
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn haversine_known_distance() {
91 let d = haversine(40.4168, -3.7038, 41.3851, 2.1734);
92 assert!((d - 500_000_f64).abs() < 50_000_f64);
93 }
94
95 #[test]
96 fn haversine_zero_distance() {
97 let d = haversine(40.4168, -3.7038, 40.4168, -3.7038);
98 assert!(d < 1_f64);
99 }
100
101 #[tokio::test]
102 async fn check_proximity_empty_when_no_location() {
103 let geo = GeoService::new();
104 let near = geo.check_proximity(1).await;
105 assert!(near.is_empty());
106 }
107}