Add EXIF search infrastructure (Phase 1 & 2)
Implements foundation for EXIF-based photo search capabilities: - Add geo.rs module with GPS distance calculations (Haversine + bounding box) - Extend FilesRequest with EXIF search parameters (camera, GPS, date, media type) - Add MediaType enum and DateTakenAsc/DateTakenDesc sort options - Create date_taken index migration for efficient date queries - Implement ExifDao methods: get_exif_batch, query_by_exif, get_camera_makes - Add FileWithMetadata struct for date-aware sorting - Implement date sorting with filename extraction fallback - Make extract_date_from_filename public for reuse Next: Integrate EXIF filtering into list_photos() and enhance get_all_tags()
This commit is contained in:
120
src/geo.rs
Normal file
120
src/geo.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
/// 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
|
||||
/// ```
|
||||
/// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user