Files
ImageApi/src/geo.rs
2025-12-19 14:20:51 -05:00

122 lines
4.1 KiB
Rust

/// Geographic calculation utilities for GPS-based search
use std::f64;
/// Calculate distance between two GPS coordinates using the Haversine formula.
/// Returns distance in kilometers.
///
/// # Arguments
/// * `lat1` - Latitude of first point in decimal degrees
/// * `lon1` - Longitude of first point in decimal degrees
/// * `lat2` - Latitude of second point in decimal degrees
/// * `lon2` - Longitude of second point in decimal degrees
///
/// # Example
/// ```
/// use image_api::geo::haversine_distance;
/// let distance = haversine_distance(37.7749, -122.4194, 34.0522, -118.2437);
/// // Distance between San Francisco and Los Angeles (~559 km)
/// ```
pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
const EARTH_RADIUS_KM: f64 = 6371.0;
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let delta_lat = (lat2 - lat1).to_radians();
let delta_lon = (lon2 - lon1).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_KM * c
}
/// Calculate bounding box for GPS radius query.
/// Returns (min_lat, max_lat, min_lon, max_lon) that encompasses the search radius.
///
/// This is used as a fast first-pass filter for GPS queries, narrowing down
/// candidates before applying the more expensive Haversine distance calculation.
///
/// # Arguments
/// * `lat` - Center latitude in decimal degrees
/// * `lon` - Center longitude in decimal degrees
/// * `radius_km` - Search radius in kilometers
///
/// # Returns
/// A tuple of (min_lat, max_lat, min_lon, max_lon) in decimal degrees
pub fn gps_bounding_box(lat: f64, lon: f64, radius_km: f64) -> (f64, f64, f64, f64) {
const EARTH_RADIUS_KM: f64 = 6371.0;
// Calculate latitude delta (same at all latitudes)
let lat_delta = (radius_km / EARTH_RADIUS_KM) * (180.0 / f64::consts::PI);
// Calculate longitude delta (varies with latitude)
let lon_delta = lat_delta / lat.to_radians().cos();
(
lat - lat_delta, // min_lat
lat + lat_delta, // max_lat
lon - lon_delta, // min_lon
lon + lon_delta, // max_lon
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_haversine_distance_sf_to_la() {
// San Francisco to Los Angeles
let distance = haversine_distance(37.7749, -122.4194, 34.0522, -118.2437);
// Should be approximately 559 km
assert!(
(distance - 559.0).abs() < 10.0,
"Distance should be ~559km, got {}",
distance
);
}
#[test]
fn test_haversine_distance_same_point() {
// Same point should have zero distance
let distance = haversine_distance(37.7749, -122.4194, 37.7749, -122.4194);
assert!(
distance < 0.001,
"Same point should have ~0 distance, got {}",
distance
);
}
#[test]
fn test_gps_bounding_box() {
// Test bounding box calculation for 10km radius around San Francisco
let (min_lat, max_lat, min_lon, max_lon) = gps_bounding_box(37.7749, -122.4194, 10.0);
// Verify the bounds are reasonable
assert!(min_lat < 37.7749, "min_lat should be less than center");
assert!(max_lat > 37.7749, "max_lat should be greater than center");
assert!(min_lon < -122.4194, "min_lon should be less than center");
assert!(max_lon > -122.4194, "max_lon should be greater than center");
// Verify bounds span roughly the right distance
let lat_span = max_lat - min_lat;
assert!(
lat_span > 0.1 && lat_span < 0.3,
"Latitude span should be reasonable for 10km"
);
}
#[test]
fn test_haversine_distance_across_equator() {
// Test across equator
let distance = haversine_distance(1.0, 0.0, -1.0, 0.0);
// Should be approximately 222 km
assert!(
(distance - 222.0).abs() < 5.0,
"Distance should be ~222km, got {}",
distance
);
}
}