From 85b47e1ffa76eaeb1d501f5f25b6efb5b969e997 Mon Sep 17 00:00:00 2001 From: Lyn Chen Date: Sat, 11 Nov 2023 12:53:31 +0800 Subject: [PATCH] feat: auto-detect gcj-02 selection --- README.md | 20 ++++-- README_en.md | 10 +-- src/core/app.rs | 60 +++++++++-------- src/core/cli.rs | 49 ++++++++------ src/exif_writer/exiftool.rs | 4 +- src/location_reader/life_path.rs | 10 +-- src/location_reader/mod.rs | 2 +- src/util/file.rs | 4 +- src/util/location.rs | 106 +++++++++++++++++++++++++++++++ src/util/mod.rs | 1 + 10 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 src/util/location.rs diff --git a/README.md b/README.md index f0449b1..7af4ce3 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ - [x] 支持 JPEG 和 PNG 及各大相机厂商的主流RAW格式 - [x] 全平台支持 - [x] 支持国策局 GCJ-02 和 WGS-84 坐标系 (解决国内坐标漂移问题) +- [x] 自动检测国内外位置, 自动转换为对应坐标系 ## DEMO ```shell ➜ nya-exif /path/to/image/folder/ -2023-11-08 15:57:30.830962000 [INFO] :Updating location for 20230908-_MGL4076.JPG -2023-11-08 15:57:30.931190000 [INFO] :Updating location for 20230908-_MGL4062.JPG -2023-11-08 15:57:30.967376000 [INFO] :Updating location for 20230908-_MGL4089.JPG -2023-11-08 15:57:30.967376000 [WARN] :Missing location for file _MGL9572.JPG, timestamp 1699257194 +2023-11-11 12:46:08.337698000 [INFO] :[20230908-_MGL4100.JPG] Location updated, lat: 34.7737885, lon: 131.9007701 +2023-11-11 12:46:08.394225000 [INFO] :[20230908-_MGL4114.JPG] Location updated, lat: 34.67844170666667, lon: 131.83647663733333 +2023-11-11 12:46:08.434180000 [INFO] :[20230908-_MGL4128.JPG] Location updated, lat: 34.68192337279844, lon: 131.8327970596869 ⠂ [00:00:04] [###########################>-----------------------------------------------] 93/233 (6.7s) ``` @@ -31,7 +31,7 @@ ```shell # macOS 下, 一生足迹启动 iCloud 云备份, 可直接运行 -nya-exif /path/to/images +nya-exif . # 其他平台下, 需要将一生足迹数据目录拷贝至本地 nya-exif -f /path/to/life-path/data /path/to/images @@ -39,7 +39,7 @@ nya-exif -f /path/to/life-path/data /path/to/images # 若 ExifTool 安装路径不在 PATH 中, 手动指定可执行文件位置 nya-exif -b /path/to/exiftool /path/to/images -# 指定目标坐标系, 默认为中国 GCJ-02 坐标系, 如果照片拍摄地为海外需要指定为 WGS-84 坐标系 +# 指定目标坐标系, 默认为自动检测, 如果在边境线附近需要手动指定 nya-exif -c wgs84 /path/to/images ``` @@ -111,7 +111,9 @@ Options: [default: 600] -c, --location-coordinate-target - [default: gcj02] + Location GPS coordinate convert target + + Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. Possible values: - wgs84: Global coordinate system @@ -150,6 +152,10 @@ Options: > [!IMPORTANT] > Location Reader返回的经纬度应该为地球坐标系(WGS84), 本工具会根据用户选择的坐标系进行转换 +## Declaimer + +本工具中附带的 `GCJ-02` 范围数据仅用于粗略地理位置判断, 不具有任何立场和政治倾向, 请勿用于其他用途 + ## License [MIT](LICENSE) ©[Lyn](mailto://i@lyn.moe) diff --git a/README_en.md b/README_en.md index cd56559..f0f8fe9 100644 --- a/README_en.md +++ b/README_en.md @@ -18,9 +18,9 @@ ```shell ➜ nya-exif /path/to/image/folder/ -2023-11-08 15:57:30.830962000 [INFO] :Updating location for 20230908-_MGL4076.JPG -2023-11-08 15:57:30.931190000 [INFO] :Updating location for 20230908-_MGL4062.JPG -2023-11-08 15:57:30.967376000 [INFO] :Updating location for 20230908-_MGL4089.JPG +2023-11-11 12:46:08.337698000 [INFO] :[20230908-_MGL4100.JPG] Location updated, lat: 34.7737885, lon: 131.9007701 +2023-11-11 12:46:08.394225000 [INFO] :[20230908-_MGL4114.JPG] Location updated, lat: 34.67844170666667, lon: 131.83647663733333 +2023-11-11 12:46:08.434180000 [INFO] :[20230908-_MGL4128.JPG] Location updated, lat: 34.68192337279844, lon: 131.8327970596869 ⠂ [00:00:04] [###########################>-----------------------------------------------] 93/233 (6.7s) ``` @@ -110,7 +110,9 @@ Options: [default: 600] -c, --location-coordinate-target - [default: gcj02] + Location GPS coordinate convert target + + Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. Possible values: - wgs84: Global coordinate system diff --git a/src/core/app.rs b/src/core/app.rs index 99a96fa..64ce55c 100644 --- a/src/core/app.rs +++ b/src/core/app.rs @@ -1,14 +1,12 @@ +use crate::exif_writer::{exiftool::ExifWriterExifTool, ExifWriterBase, ExifWriterParam}; +use crate::location_reader::{life_path, LocationReaderBase, LocationReaderParam}; +use crate::util::{file, location as lc}; use clap::ValueEnum; +use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use simple_log::log::{info, warn}; use std::fmt::Write; use std::path::{Path, PathBuf}; -use simple_log::log::{warn, info, error}; use undrift_gps::wgs_to_gcj; -use indicatif::{ProgressBar, ProgressState, ProgressStyle}; -use std::sync::mpsc::channel; -use ctrlc; -use crate::exif_writer::{exiftool::ExifWriterExifTool, ExifWriterBase, ExifWriterParam}; -use crate::location_reader::{life_path, LocationReaderBase, LocationReaderParam}; -use crate::util::file; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] pub enum ExifWriterType { @@ -32,24 +30,19 @@ pub enum LocationGpsCoordinateTarget { #[derive(Debug)] pub struct AppParams { - pub operate_dir: PathBuf, + pub operate_dir: Vec, pub recursive: bool, pub writer_type: ExifWriterType, pub writer_bin_path: Option, pub location_reader_type: LocationReaderType, pub location_file_path: Option, pub location_max_interval: u32, - pub location_gps_coordinate_target: LocationGpsCoordinateTarget, + pub location_gps_coordinate_target: Option, pub overwrite_original: bool, pub time_offset: i32, } pub fn run(params: AppParams) { - let (tx, rx) = channel(); - - ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) - .expect("Error setting Ctrl-C handler"); - let exif_param = ExifWriterParam { binary_path: params.writer_bin_path.clone(), }; @@ -74,7 +67,11 @@ pub fn run(params: AppParams) { } } - let fi = file::read_dir_files(params.operate_dir.as_ref(), true, true).unwrap(); + let mut fi = vec![]; + for dir in params.operate_dir { + let fi_ = file::read_dir_files(dir.as_ref(), true, true).unwrap(); + fi.extend(fi_); + } let pb = ProgressBar::new(fi.len() as u64); pb.set_style( @@ -96,9 +93,24 @@ pub fn run(params: AppParams) { let time = exiftool.read_timestamp(filename); let mut location = location_reader.get_location(time as i32); - match params.location_gps_coordinate_target { - LocationGpsCoordinateTarget::GCJ02 => { - if location.is_some() { + if location.is_some() { + if params.location_gps_coordinate_target.is_some() { + match params.location_gps_coordinate_target.unwrap() { + LocationGpsCoordinateTarget::GCJ02 => { + let mut lo = location.unwrap(); + let (lat, lon) = wgs_to_gcj(lo.lat, lo.lon); + lo.lat = lat; + lo.lon = lon; + + location = Some(lo); + } + _ => {} + } + } else { + let llo = location.clone().unwrap(); + let result = lc::is_point_in_gcj_region((llo.lat, llo.lon)); + + if result { let mut lo = location.unwrap(); let (lat, lon) = wgs_to_gcj(lo.lat, lo.lon); lo.lat = lat; @@ -107,7 +119,6 @@ pub fn run(params: AppParams) { location = Some(lo); } } - _ => {} } if location.is_some() { @@ -116,7 +127,10 @@ pub fn run(params: AppParams) { pb.suspend(|| { let filename = Path::new(filename).file_name().unwrap().to_str().unwrap(); - info!("[{}] Location updated, lat: {}, lon: {}", filename, location.lat, location.lon); + info!( + "[{}] Location updated, lat: {}, lon: {}", + filename, location.lat, location.lon + ); }); } else { pb.suspend(|| { @@ -126,12 +140,6 @@ pub fn run(params: AppParams) { } pb.set_position(now_state); - - if rx.try_recv().is_ok() { - error!("Stopped by user"); - pb.finish_with_message("Stopped"); - return; - } } pb.finish_with_message("Finished"); diff --git a/src/core/cli.rs b/src/core/cli.rs index 3dc6378..c0130b2 100644 --- a/src/core/cli.rs +++ b/src/core/cli.rs @@ -1,7 +1,6 @@ use clap::Parser; -use std::env; use std::path::PathBuf; -use simple_log::log::debug; +use simple_log::log::{debug, error}; use simple_log::LogConfigBuilder; use crate::core::app::{self, ExifWriterType, LocationReaderType, LocationGpsCoordinateTarget}; @@ -12,7 +11,7 @@ use crate::core::app::{self, ExifWriterType, LocationReaderType, LocationGpsCoor #[command(version)] struct Cli { /// Path to photography files - path: Option, + path: Vec, /// Turn on recursive mode #[arg(short, long, default_value_t = true)] @@ -53,8 +52,11 @@ struct Cli { #[arg(short = 'i', long, default_value_t = 600)] location_max_interval: u32, - #[arg(short = 'c', long, value_enum, default_value_t = LocationGpsCoordinateTarget::GCJ02)] - location_coordinate_target: LocationGpsCoordinateTarget, + /// Location GPS coordinate convert target + /// + /// Specifies the target coordinate system for converting GPS coordinates. Default is Auto-detect. + #[arg(short = 'c', long, value_enum)] + location_coordinate_target: Option, /// Overwrite original file #[arg(short, long, default_value_t = true)] @@ -77,14 +79,14 @@ pub fn run() { let cli = Cli::parse(); let mut param = app::AppParams { - operate_dir: env::current_dir().unwrap(), + operate_dir: Vec::new(), recursive: true, writer_type: app::ExifWriterType::Exiftool, writer_bin_path: None, location_reader_type: app::LocationReaderType::LifePath, location_file_path: None, location_max_interval: 1800, - location_gps_coordinate_target: app::LocationGpsCoordinateTarget::WGS84, + location_gps_coordinate_target: None, overwrite_original: false, time_offset: 0, }; @@ -101,43 +103,48 @@ pub fn run() { simple_log::new(config).expect("Failed to init log"); } - if let Some(pwd) = cli.path { - debug!("Value for path: {}", pwd); - param.operate_dir = PathBuf::from(pwd); + if cli.path.len() > 0 { + debug!("[Arg] Value for path: {:?}", cli.path); + param.operate_dir = cli.path.iter().map(|x| PathBuf::from(x)).collect(); + } else { + error!("No path specified"); + std::process::exit(1); } - debug!("Value for recursive: {}", cli.recursive); + debug!("[Arg] Value for recursive: {}", cli.recursive); param.recursive = cli.recursive; - debug!("Value for writer_type: {:?}", cli.writer_type); + debug!("[Arg] Value for writer_type: {:?}", cli.writer_type); param.writer_type = cli.writer_type; if let Some(writer_bin_path) = cli.writer_bin_path { - debug!("Value for writer_bin_path: {}", writer_bin_path); + debug!("[Arg] Value for writer_bin_path: {}", writer_bin_path); param.writer_bin_path = Some(PathBuf::from(writer_bin_path)); } - debug!("Value for location_reader_type: {:?}", cli.location_reader_type); + debug!("[Arg] Value for location_reader_type: {:?}", cli.location_reader_type); param.location_reader_type = cli.location_reader_type; if let Some(location_file_path) = cli.location_file_path { - debug!("Value for location_file_path: {}", location_file_path); + debug!("[Arg] Value for location_file_path: {}", location_file_path); param.location_file_path = Some(PathBuf::from(location_file_path)); } - debug!("Value for location_max_interval: {}", cli.location_max_interval); + debug!("[Arg] Value for location_max_interval: {}", cli.location_max_interval); param.location_max_interval = cli.location_max_interval; - debug!("Value for location_gps_coordinate_target: {:?}", cli.location_coordinate_target); - param.location_gps_coordinate_target = cli.location_coordinate_target; + if let Some(location_coordinate_target) = cli.location_coordinate_target { + debug!("[Arg] Value for location_gps_coordinate_target: {:?}", location_coordinate_target); + param.location_gps_coordinate_target = Some(location_coordinate_target); + } - debug!("Value for overwrite_original: {}", cli.overwrite_original); + debug!("[Arg] Value for overwrite_original: {}", cli.overwrite_original); param.overwrite_original = cli.overwrite_original; - debug!("Value for time_offset: {}", cli.time_offset); + debug!("[Arg] Value for time_offset: {}", cli.time_offset); param.time_offset = cli.time_offset; - debug!("Value for app params: {:?}", param); + debug!("[Arg] Value for app params: {:?}", param); app::run(param); } \ No newline at end of file diff --git a/src/exif_writer/exiftool.rs b/src/exif_writer/exiftool.rs index 04a45b5..3a948c6 100644 --- a/src/exif_writer/exiftool.rs +++ b/src/exif_writer/exiftool.rs @@ -64,7 +64,7 @@ impl ExifWriterExifTool { result.push_str(&line); } - debug!("Exiftool arg: {:?}, result: {}", args, result); + debug!("[Exiftool] arg: {:?}, result: {}", args, result); result } @@ -95,7 +95,7 @@ impl ExifWriterBase for ExifWriterExifTool { let dt = DateTime::parse_from_str(&time_string, "%Y:%m:%d %H:%M:%S %z") .expect("Failed to parse date time string"); - debug!("Exiftool read timestamp: {}, time_string: {}", dt.timestamp(), time_string); + debug!("[Exiftool] time_string: {}, parse timestamp: {}", time_string, dt.timestamp()); dt.timestamp() } diff --git a/src/location_reader/life_path.rs b/src/location_reader/life_path.rs index f8dcdb5..b4cae05 100644 --- a/src/location_reader/life_path.rs +++ b/src/location_reader/life_path.rs @@ -30,10 +30,10 @@ impl LocationReaderLiftPath { } file_path.push("backUpData.csv"); - debug!("LifePath folder path: {:?}", file_path); + debug!("[LifePath] folder path: {:?}", file_path); if !file_path.exists() { - error!("LifePath csv file not exists, path {:?}", file_path); + error!("[LifePath] CSV file not exists, path {:?}", file_path); panic!(); } @@ -76,8 +76,8 @@ impl LocationReaderBase for LocationReaderLiftPath { .from_reader(&self.file); let record2 = rdr.records().next()?.unwrap(); - debug!("record1: {:?}", record1); - debug!("record2: {:?}", record2); + debug!("[LifePath] search record1: {:?}", record1); + debug!("[LifePath] search record2: {:?}", record2); let d1 = (record1[0].parse::().unwrap() - timestamp).abs(); let d2 = (record2[0].parse::().unwrap() - timestamp).abs(); @@ -90,7 +90,7 @@ impl LocationReaderBase for LocationReaderLiftPath { let alt_mid = record1[10].parse::().unwrap() * p1 + record2[10].parse::().unwrap() * p2; let confidence_radius_min = record1[5].parse::().unwrap().min(record2[5].parse::().unwrap()); - debug!("time_mid: {}, max interval: {}", time_mid, self.param.max_interval); + debug!("[LifePath] time_mid: {}, max interval: {}", time_mid, self.param.max_interval); if (time_mid - timestamp).abs() > self.param.max_interval as i32 { return None; diff --git a/src/location_reader/mod.rs b/src/location_reader/mod.rs index b992147..2233d74 100644 --- a/src/location_reader/mod.rs +++ b/src/location_reader/mod.rs @@ -7,7 +7,7 @@ pub struct LocationReaderParam { pub max_interval: u32, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct LocationReaderResult { pub lat: f64, pub lon: f64, diff --git a/src/util/file.rs b/src/util/file.rs index b6f6ac6..8f7b29c 100644 --- a/src/util/file.rs +++ b/src/util/file.rs @@ -7,7 +7,7 @@ use simple_log::log::debug; pub fn read_dir_files(dir: &Path, full_path: bool, recursive: bool) -> io::Result> { let exts = vec!["jpg", "jpeg", "png", "cr3", "dng", "heic", "RW2", "raw", "raf", "arw", "arq", "nef", "nrw"]; - debug!("Reading dir: {:?}", dir); + debug!("[Util readDir] Reading dir: {:?}", dir); let mut files = Vec::new(); @@ -24,7 +24,7 @@ pub fn read_dir_files(dir: &Path, full_path: bool, recursive: bool) -> io::Resul } let ext = path.extension().unwrap_or("".to_owned().as_ref()).to_string_lossy().to_lowercase(); if !exts.contains(&ext.as_ref()) { - debug!("Skipping file: {:?}", path); + debug!("[Util readDir] Skipping file: {:?}", path); continue; } diff --git a/src/util/location.rs b/src/util/location.rs new file mode 100644 index 0000000..d3420a6 --- /dev/null +++ b/src/util/location.rs @@ -0,0 +1,106 @@ +use simple_log::log::debug; + +// Approximate GCJ region in (latitude, longitude) pairs +static GCJ_REGION: &'static [(f64, f64)] = &[ + (52.731367549831, 120.18298955774142), + (53.63032135052725, 122.50163662235207), + (53.26176144514083, 125.77607688660949), + (49.793642660678515, 127.87680506936519), + (49.407718349606306, 129.66811795485958), + (47.998560826930955, 131.530512732968), + (48.51048270954458, 134.84037096447878), + (45.12562855907086, 133.32895840353396), + (44.79216287150945, 131.28848995673235), + (42.63680250697947, 130.45592176815663), + (42.99527458867438, 129.91790514760166), + (42.04761801712594, 128.0687300505346), + (41.45987408769909, 128.10876970001843), + (41.780407957507286, 126.81985564688428), + (38.461741231751844, 121.50306067363671), + (37.414082923391454, 122.93061875172674), + (30.674023244049664, 123.07426160685057), + (24.6033327288128, 119.22457427978614), + (21.179240961699527, 112.43694704179752), + (17.673075974060612, 109.48709815312112), + (19.138508820560002, 108.18083521881559), + (21.39327464004528, 108.15083770831288), + (22.080063041679676, 106.60666332280034), + (22.71632309622958, 106.5008040544447), + (23.13617888669703, 105.0537266093335), + (22.32102047283515, 101.78903194655028), + (21.091729915864516, 101.81916659472037), + (21.497462709277563, 100.27712620789795), + (22.172525330672745, 99.21924485723063), + (23.922710480065003, 97.59174752286235), + (24.7766324211775, 97.52779222878526), + (25.979741935612175, 98.59304557492919), + (27.518813240263356, 98.41488901761588), + (29.255978242960396, 96.05290813407619), + (28.98351539963584, 94.3743597614614), + (27.73202645570489, 91.91576503922244), + (27.954988751231465, 89.43637256086063), + (27.305474672237125, 88.87924298495538), + (27.926305488252357, 85.9660573850956), + (30.018093251412424, 81.1821861261154), + (31.29101575822806, 78.8959522774237), + (32.51080027519665, 78.40327588060015), + (32.70102288420409, 79.42731032370726), + (34.73972040936371, 78.19756139986893), + (35.46498638604698, 78.08744745345531), + (35.827127091240776, 76.13888392627935), + (36.12722522700187, 75.92489090497999), + (36.74619160023912, 75.56385329098687), + (37.09152700706339, 74.49469911660775), + (37.36092398871107, 75.07085973927778), + (38.52410020540065, 73.96571879042757), + (39.45869189286305, 73.55981403057588), + (40.48588468250351, 74.8404885479942), + (41.4079098041435, 78.18892031992831), + (42.209549490371245, 80.1685385590239), + (43.0232779792319, 80.3924877595656), + (44.92499622938564, 79.91450165082203), + (45.27242675378137, 82.50522457891103), + (47.22146706317084, 82.95321261550617), + (47.06392532516057, 85.49226819096441), + (48.43342505864067, 85.73634505323675), + (49.146033236672466, 86.88083279984656), + (49.157812554992795, 87.77826911573229), + (47.862960480730074, 90.01921884921369), + (46.02248151184977, 91.03156364579425), + (45.21092618689381, 90.84795164769706), + (44.33814457729536, 95.3781833450865), + (42.77559669230801, 96.46889449961684), + (42.50247036887638, 101.82589397754059), + (41.939925797616525, 105.58392770472803), + (42.757160841682044, 110.1889382502376), + (43.753981039668, 111.87813257735903), + (45.081602850466446, 111.76814565537664), + (46.755407823982075, 119.02228883676571), + (47.248791064053286, 119.54348219864653), + (48.022727706580255, 117.82995057487629), + (47.630522239677866, 117.36286596647822), + (47.91831453430476, 115.56839450099663), + (49.83589917843746, 116.6729235427556), + (50.3781951866589, 119.13757348710827), + (52.11976005858193, 120.72835099286257), + (52.5816732367262, 120.15157047622971) +]; + +pub fn is_point_in_gcj_region(point: (f64, f64)) -> bool { + let mut count = 0; + let (px, py) = point; + let mut j = GCJ_REGION.len() - 1; + for i in 0..GCJ_REGION.len() { + let (sx, sy) = GCJ_REGION[i]; + let (ex, ey) = GCJ_REGION[j]; + if (sy < py && ey >= py) || (ey < py && sy >= py) { + if sx + (py - sy) / (ey - sy) * (ex - sx) < px { + count += 1; + } + } + j = i; + } + + debug!("[Util Loca inGcj] Pair {:?}, count {}, in region: {}", point, count, count % 2 != 0); + count % 2 != 0 +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 2e172cd..fd6de94 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1 +1,2 @@ pub mod file; +pub mod location;