diff --git a/Cargo.lock b/Cargo.lock index f5b328b..82f1707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,7 @@ dependencies = [ "humantime", "ignore", "jemallocator", + "kamadak-exif", "libc", "lscolors", "nix 0.27.1", @@ -474,6 +475,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -522,6 +532,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "nix" version = "0.24.3" diff --git a/Cargo.toml b/Cargo.toml index 0d18b20..3da3d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ normpath = "1.1.1" crossbeam-channel = "0.5.8" clap_complete = {version = "4.4.4", optional = true} faccess = "0.2.4" +kamadak-exif = "0.5.5" [patch.crates-io] ignore = { git = "https://github.com/tavianator/ripgrep", branch = "fd" } diff --git a/src/filter/geo_location.rs b/src/filter/geo_location.rs new file mode 100644 index 0000000..120af1b --- /dev/null +++ b/src/filter/geo_location.rs @@ -0,0 +1,129 @@ +use std::{ + f32::consts::PI, + fmt::{Display, Formatter}, + fs::File, + io::BufReader, + path::Path, +}; + +use exif::{Exif, In, Reader, Tag}; +use regex::Regex; + +/// Struct representing a geo location +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct GeoLocation { + pub latitude: f32, + pub longitude: f32, +} + +/// Display trait implementation for GeoLocation +impl Display for GeoLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[lat={} lon={}]", self.latitude, self.longitude,) + } +} + +impl GeoLocation { + ///Computes the distance in meters using the Haversine formula + pub fn distance_to(&self, other: &GeoLocation) -> f32 { + let r = 6378137.; // radius of earth in meters + let d_lat = other.latitude * PI / 180. - self.latitude * PI / 180.; + let d_lon = other.longitude * PI / 180. - self.longitude * PI / 180.; + let a = f32::sin(d_lat / 2.) * f32::sin(d_lat / 2.) + + f32::cos(self.latitude * PI / 180.) + * f32::cos(other.latitude * PI / 180.) + * f32::sin(d_lon / 2.) + * f32::sin(d_lon / 2.); + let c = 2. * f32::atan2(f32::sqrt(a), f32::sqrt(1. - a)); + return r * c; + } +} + +/// Converts Degrees Minutes Seconds To Decimal Degrees +fn dms_to_dd(dms_string: &str, dms_ref: &str) -> Option { + // Depending on the dms ref the value has to be multiplied by -1 + let dms_ref_multiplier = match dms_ref { + "S" | "W" => -1.0, + _ => 1.0, + }; + + let dms_parse_pattern: Regex = Regex::new( + // e.g.: 7 deg 33 min 55.5155 sec or 7 deg 33 min 55 sec + r"(?P\d+\.?\d*) deg (?P\d+) min (?P\d+\.?\d*) sec", + ) + .unwrap(); + let Some(pattern_match) = dms_parse_pattern.captures(dms_string) else { + return None; + }; + + let Some(deg) = pattern_match + .name("deg") + .map(|cap| cap.as_str().parse::().unwrap()) + else { + return None; + }; + let Some(min) = pattern_match + .name("min") + .map(|cap| cap.as_str().parse::().unwrap()) + else { + return None; + }; + let Some(sec) = pattern_match + .name("sec") + .map(|cap| cap.as_str().parse::().unwrap()) + else { + return None; + }; + + Some(dms_ref_multiplier * (deg + (min / 60.0) + (sec / 3600.0))) +} + +impl GeoLocation { + /// Detects the location from the exif data + /// If the location is not found, the location is set to None + fn from_exif(exif_data: &Exif) -> Option { + let Some(latitude) = exif_data.get_field(Tag::GPSLatitude, In::PRIMARY) else { + return None; + }; + let Some(latitude_ref) = exif_data.get_field(Tag::GPSLatitudeRef, In::PRIMARY) else { + return None; + }; + let Some(longitude) = exif_data.get_field(Tag::GPSLongitude, In::PRIMARY) else { + return None; + }; + let Some(longitude_ref) = exif_data.get_field(Tag::GPSLongitudeRef, In::PRIMARY) else { + return None; + }; + let Some(dd_lat) = dms_to_dd( + &latitude.display_value().to_string(), + &latitude_ref.display_value().to_string(), + ) else { + return None; + }; + let Some(dd_lon) = dms_to_dd( + &longitude.display_value().to_string(), + &longitude_ref.display_value().to_string(), + ) else { + return None; + }; + + Some(GeoLocation { + latitude: dd_lat, + longitude: dd_lon, + }) + } +} + +pub fn exif_geo_distance(path: &Path, reference: &GeoLocation) -> Option { + let Ok(file) = File::open(path) else { + return None; + }; + let Ok(exif) = Reader::new().read_from_container(&mut BufReader::new(&file)) else { + return None; + }; + let Some(location) = GeoLocation::from_exif(&exif) else { + return None; + }; + let distance_meter = location.distance_to(&reference); + return Some(distance_meter); +} diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 5e45d3b..1700907 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -1,9 +1,11 @@ +pub use self::geo_location::{exif_geo_distance, GeoLocation}; pub use self::size::SizeFilter; pub use self::time::TimeFilter; #[cfg(unix)] pub use self::owner::OwnerFilter; +mod geo_location; mod size; mod time; diff --git a/src/walk.rs b/src/walk.rs index 691c5d0..c4c2017 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -21,6 +21,7 @@ use crate::error::print_error; use crate::exec; use crate::exit_codes::{merge_exitcodes, ExitCode}; use crate::filesystem; +use crate::filter::{exif_geo_distance, GeoLocation}; use crate::output; /// The receiver thread can either be buffering results or directly streaming to the console. @@ -502,6 +503,19 @@ impl WorkerState { } } + let reference = GeoLocation { + latitude: 47.3, + longitude: 11.3, + }; + let max_distance = 500.; + if let Some(distance) = exif_geo_distance(entry.path(), &reference) { + if distance > max_distance { + return WalkState::Continue; + } + } else { + return WalkState::Continue; + } + if config.is_printing() { if let Some(ls_colors) = &config.ls_colors { // Compute colors in parallel