1use serde::{Deserialize, Serialize};
42use std::collections::HashMap;
43
44const EARTH_RADIUS_KM: f64 = 6371.0;
46
47#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
49pub struct GeoLocation {
50 pub latitude: f64,
52 pub longitude: f64,
54}
55
56impl GeoLocation {
57 #[must_use]
63 pub fn new(latitude: f64, longitude: f64) -> Self {
64 assert!(
65 (-90.0..=90.0).contains(&latitude),
66 "Latitude must be between -90 and 90 degrees"
67 );
68 assert!(
69 (-180.0..=180.0).contains(&longitude),
70 "Longitude must be between -180 and 180 degrees"
71 );
72
73 Self {
74 latitude,
75 longitude,
76 }
77 }
78
79 #[must_use]
83 #[inline]
84 pub fn distance_to(&self, other: &GeoLocation) -> f64 {
85 haversine_distance(self, other)
86 }
87
88 #[must_use]
90 #[inline]
91 pub fn is_within(&self, other: &GeoLocation, radius_km: f64) -> bool {
92 self.distance_to(other) <= radius_km
93 }
94
95 #[must_use]
97 #[inline]
98 pub fn bearing_to(&self, other: &GeoLocation) -> f64 {
99 let lat1 = self.latitude.to_radians();
100 let lat2 = other.latitude.to_radians();
101 let delta_lon = (other.longitude - self.longitude).to_radians();
102
103 let y = delta_lon.sin() * lat2.cos();
104 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * delta_lon.cos();
105
106 let bearing = y.atan2(x).to_degrees();
107 (bearing + 360.0) % 360.0
108 }
109}
110
111#[must_use]
115pub fn haversine_distance(loc1: &GeoLocation, loc2: &GeoLocation) -> f64 {
116 let lat1 = loc1.latitude.to_radians();
117 let lat2 = loc2.latitude.to_radians();
118 let delta_lat = (loc2.latitude - loc1.latitude).to_radians();
119 let delta_lon = (loc2.longitude - loc1.longitude).to_radians();
120
121 let a =
122 (delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
123 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
124
125 EARTH_RADIUS_KM * c
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct GeoPeer {
131 pub peer_id: String,
133 pub location: GeoLocation,
135 pub region: String,
137 pub latency_ms: f64,
139 pub bandwidth_mbps: f64,
141}
142
143impl GeoPeer {
144 #[must_use]
146 #[inline]
147 pub fn distance_to(&self, target: &GeoLocation) -> f64 {
148 self.location.distance_to(target)
149 }
150
151 #[must_use]
153 #[inline]
154 pub fn geo_score(&self, target: &GeoLocation) -> f64 {
155 let distance = self.distance_to(target);
156 let distance_score = 1.0 / (1.0 + distance / 1000.0); let latency_score = 1.0 / (1.0 + self.latency_ms / 100.0); 0.6 * distance_score + 0.4 * latency_score
161 }
162}
163
164#[derive(Debug, Clone)]
166pub struct GeoConfig {
167 pub preferred_radius_km: f64,
169 pub max_distance_km: f64,
171 pub enable_region_grouping: bool,
173 pub min_region_diversity: usize,
175 pub distance_weight: f64,
177}
178
179impl Default for GeoConfig {
180 fn default() -> Self {
181 Self {
182 preferred_radius_km: 500.0, max_distance_km: 10000.0, enable_region_grouping: true,
185 min_region_diversity: 2, distance_weight: 0.6, }
188 }
189}
190
191pub struct GeoSelector {
193 config: GeoConfig,
195 peers: HashMap<String, GeoPeer>,
197 regions: HashMap<String, Vec<String>>,
199}
200
201impl GeoSelector {
202 #[must_use]
204 pub fn new(config: GeoConfig) -> Self {
205 Self {
206 config,
207 peers: HashMap::new(),
208 regions: HashMap::new(),
209 }
210 }
211
212 pub fn add_peer(&mut self, peer: GeoPeer) {
214 self.regions
216 .entry(peer.region.clone())
217 .or_default()
218 .push(peer.peer_id.clone());
219
220 self.peers.insert(peer.peer_id.clone(), peer);
222 }
223
224 pub fn remove_peer(&mut self, peer_id: &str) -> Option<GeoPeer> {
226 if let Some(peer) = self.peers.remove(peer_id) {
227 if let Some(region_peers) = self.regions.get_mut(&peer.region) {
229 region_peers.retain(|id| id != peer_id);
230 if region_peers.is_empty() {
231 self.regions.remove(&peer.region);
232 }
233 }
234 Some(peer)
235 } else {
236 None
237 }
238 }
239
240 #[must_use]
242 #[inline]
243 pub fn find_nearest(&self, target: &GeoLocation, n: usize) -> Vec<GeoPeer> {
244 let mut peers_with_distance: Vec<(GeoPeer, f64)> = self
245 .peers
246 .values()
247 .map(|peer| {
248 let distance = peer.distance_to(target);
249 (peer.clone(), distance)
250 })
251 .collect();
252
253 peers_with_distance.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
255
256 peers_with_distance
258 .into_iter()
259 .take(n)
260 .map(|(peer, _)| peer)
261 .collect()
262 }
263
264 #[must_use]
266 #[inline]
267 pub fn find_within_radius(&self, target: &GeoLocation, radius_km: f64) -> Vec<GeoPeer> {
268 self.peers
269 .values()
270 .filter(|peer| peer.distance_to(target) <= radius_km)
271 .cloned()
272 .collect()
273 }
274
275 #[must_use]
277 #[inline]
278 pub fn select_best(&self, target: &GeoLocation, n: usize) -> Vec<GeoPeer> {
279 let mut peers_with_score: Vec<(GeoPeer, f64)> = self
280 .peers
281 .values()
282 .map(|peer| {
283 let score = peer.geo_score(target);
284 (peer.clone(), score)
285 })
286 .collect();
287
288 peers_with_score.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
290
291 peers_with_score
292 .into_iter()
293 .take(n)
294 .map(|(peer, _)| peer)
295 .collect()
296 }
297
298 #[must_use]
300 #[inline]
301 pub fn select_diverse(&self, target: &GeoLocation, n: usize) -> Vec<GeoPeer> {
302 if !self.config.enable_region_grouping {
303 return self.select_best(target, n);
304 }
305
306 let mut selected = Vec::new();
307 let mut used_regions = std::collections::HashSet::new();
308
309 let mut peers_with_score: Vec<(GeoPeer, f64)> = self
311 .peers
312 .values()
313 .map(|peer| {
314 let score = peer.geo_score(target);
315 (peer.clone(), score)
316 })
317 .collect();
318
319 peers_with_score.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
320
321 for (peer, _) in &peers_with_score {
323 if !used_regions.contains(&peer.region) {
324 selected.push(peer.clone());
325 used_regions.insert(peer.region.clone());
326
327 if selected.len() >= n {
328 return selected;
329 }
330 }
331 }
332
333 for (peer, _) in peers_with_score {
335 if selected.iter().any(|p| p.peer_id == peer.peer_id) {
336 continue;
337 }
338 selected.push(peer);
339 if selected.len() >= n {
340 break;
341 }
342 }
343
344 selected
345 }
346
347 #[must_use]
349 #[inline]
350 pub fn get_peers_by_region(&self, region: &str) -> Vec<GeoPeer> {
351 if let Some(peer_ids) = self.regions.get(region) {
352 peer_ids
353 .iter()
354 .filter_map(|id| self.peers.get(id))
355 .cloned()
356 .collect()
357 } else {
358 Vec::new()
359 }
360 }
361
362 #[must_use]
364 #[inline]
365 pub fn get_regions(&self) -> Vec<String> {
366 self.regions.keys().cloned().collect()
367 }
368
369 #[must_use]
371 pub fn get_geo_stats(&self) -> GeoStats {
372 let mut region_counts = HashMap::new();
373 for (region, peers) in &self.regions {
374 region_counts.insert(region.clone(), peers.len());
375 }
376
377 let total_peers = self.peers.len();
378 let total_regions = self.regions.len();
379
380 GeoStats {
381 total_peers,
382 total_regions,
383 peers_per_region: region_counts,
384 avg_peers_per_region: if total_regions > 0 {
385 total_peers as f64 / total_regions as f64
386 } else {
387 0.0
388 },
389 }
390 }
391
392 #[must_use]
394 #[inline]
395 pub fn peer_count(&self) -> usize {
396 self.peers.len()
397 }
398
399 #[must_use]
401 #[inline]
402 pub fn get_peer(&self, peer_id: &str) -> Option<&GeoPeer> {
403 self.peers.get(peer_id)
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct GeoStats {
410 pub total_peers: usize,
412 pub total_regions: usize,
414 pub peers_per_region: HashMap<String, usize>,
416 pub avg_peers_per_region: f64,
418}
419
420#[must_use]
422pub fn midpoint(loc1: &GeoLocation, loc2: &GeoLocation) -> GeoLocation {
423 let lat1 = loc1.latitude.to_radians();
424 let lon1 = loc1.longitude.to_radians();
425 let lat2 = loc2.latitude.to_radians();
426 let lon2 = loc2.longitude.to_radians();
427
428 let bx = lat2.cos() * (lon2 - lon1).cos();
429 let by = lat2.cos() * (lon2 - lon1).sin();
430
431 let lat3 = (lat1.sin() + lat2.sin()).atan2(((lat1.cos() + bx).powi(2) + by.powi(2)).sqrt());
432 let lon3 = lon1 + by.atan2(lat1.cos() + bx);
433
434 GeoLocation {
435 latitude: lat3.to_degrees(),
436 longitude: lon3.to_degrees(),
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_geo_location_creation() {
446 let loc = GeoLocation::new(37.7749, -122.4194);
447 assert_eq!(loc.latitude, 37.7749);
448 assert_eq!(loc.longitude, -122.4194);
449 }
450
451 #[test]
452 #[should_panic]
453 fn test_invalid_latitude() {
454 let _ = GeoLocation::new(91.0, 0.0);
455 }
456
457 #[test]
458 fn test_haversine_distance() {
459 let sf = GeoLocation::new(37.7749, -122.4194);
461 let ny = GeoLocation::new(40.7128, -74.0060);
462
463 let distance = sf.distance_to(&ny);
464 assert!((distance - 4130.0).abs() < 50.0);
466 }
467
468 #[test]
469 fn test_same_location_distance() {
470 let loc = GeoLocation::new(0.0, 0.0);
471 assert_eq!(loc.distance_to(&loc), 0.0);
472 }
473
474 #[test]
475 fn test_is_within_radius() {
476 let loc1 = GeoLocation::new(37.7749, -122.4194);
477 let loc2 = GeoLocation::new(37.3382, -121.8863);
478
479 assert!(loc1.is_within(&loc2, 100.0));
481 assert!(!loc1.is_within(&loc2, 50.0));
482 }
483
484 #[test]
485 fn test_geo_selector() {
486 let config = GeoConfig::default();
487 let mut selector = GeoSelector::new(config);
488
489 let peer1 = GeoPeer {
490 peer_id: "peer1".to_string(),
491 location: GeoLocation::new(37.7749, -122.4194),
492 region: "us-west".to_string(),
493 latency_ms: 50.0,
494 bandwidth_mbps: 100.0,
495 };
496
497 let peer2 = GeoPeer {
498 peer_id: "peer2".to_string(),
499 location: GeoLocation::new(40.7128, -74.0060),
500 region: "us-east".to_string(),
501 latency_ms: 120.0,
502 bandwidth_mbps: 100.0,
503 };
504
505 selector.add_peer(peer1);
506 selector.add_peer(peer2);
507
508 assert_eq!(selector.peer_count(), 2);
509
510 let target = GeoLocation::new(37.3382, -121.8863);
512 let nearest = selector.find_nearest(&target, 1);
513
514 assert_eq!(nearest.len(), 1);
515 assert_eq!(nearest[0].peer_id, "peer1");
516 }
517
518 #[test]
519 fn test_region_grouping() {
520 let config = GeoConfig::default();
521 let mut selector = GeoSelector::new(config);
522
523 selector.add_peer(GeoPeer {
524 peer_id: "peer1".to_string(),
525 location: GeoLocation::new(37.7749, -122.4194),
526 region: "us-west".to_string(),
527 latency_ms: 50.0,
528 bandwidth_mbps: 100.0,
529 });
530
531 let region_peers = selector.get_peers_by_region("us-west");
532 assert_eq!(region_peers.len(), 1);
533
534 let regions = selector.get_regions();
535 assert!(regions.contains(&"us-west".to_string()));
536 }
537
538 #[test]
539 fn test_midpoint() {
540 let loc1 = GeoLocation::new(0.0, 0.0);
541 let loc2 = GeoLocation::new(0.0, 10.0);
542
543 let mid = midpoint(&loc1, &loc2);
544 assert!((mid.latitude - 0.0).abs() < 0.01);
545 assert!((mid.longitude - 5.0).abs() < 0.01);
546 }
547
548 #[test]
549 fn test_bearing() {
550 let loc1 = GeoLocation::new(0.0, 0.0);
551 let loc2 = GeoLocation::new(1.0, 0.0);
552
553 let bearing = loc1.bearing_to(&loc2);
554 assert!((bearing - 0.0).abs() < 1.0 || (bearing - 360.0).abs() < 1.0);
556 }
557}