Skip to main content

exiftool_rs_wrapper/
geo.rs

1//! 地理信息模块
2//!
3//! 支持地理标记、反向地理编码等功能
4
5use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::types::TagId;
8use std::path::Path;
9
10/// GPS 坐标
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct GpsCoordinate {
13    /// 纬度(-90 到 90)
14    pub latitude: f64,
15    /// 经度(-180 到 180)
16    pub longitude: f64,
17    /// 海拔(米,可选)
18    pub altitude: Option<f64>,
19}
20
21impl GpsCoordinate {
22    /// 创建新的 GPS 坐标
23    pub fn new(latitude: f64, longitude: f64) -> Result<Self> {
24        if !(-90.0..=90.0).contains(&latitude) {
25            return Err(Error::invalid_arg("Latitude must be between -90 and 90"));
26        }
27        if !(-180.0..=180.0).contains(&longitude) {
28            return Err(Error::invalid_arg("Longitude must be between -180 and 180"));
29        }
30
31        Ok(Self {
32            latitude,
33            longitude,
34            altitude: None,
35        })
36    }
37
38    /// 设置海拔
39    pub fn with_altitude(mut self, altitude: f64) -> Self {
40        self.altitude = Some(altitude);
41        self
42    }
43
44    /// 格式化为 ExifTool 格式
45    pub fn format(&self) -> (String, String, Option<String>) {
46        let lat_ref = if self.latitude >= 0.0 { "N" } else { "S" };
47        let lon_ref = if self.longitude >= 0.0 { "E" } else { "W" };
48
49        let lat_val = self.latitude.abs();
50        let lon_val = self.longitude.abs();
51
52        let lat_str = format!("{:.6}", lat_val);
53        let lon_str = format!("{:.6}", lon_val);
54
55        let alt_str = self.altitude.map(|a| format!("{:.2}", a));
56
57        (
58            format!("{} {}", lat_str, lat_ref),
59            format!("{} {}", lon_str, lon_ref),
60            alt_str,
61        )
62    }
63}
64
65/// 地理编码结果
66#[derive(Debug, Clone)]
67pub struct GeocodeResult {
68    /// 城市
69    pub city: Option<String>,
70    /// 区域/州
71    pub region: Option<String>,
72    /// 国家
73    pub country: Option<String>,
74    /// 国家代码
75    pub country_code: Option<String>,
76    /// 完整地址
77    pub address: Option<String>,
78}
79
80/// 地理信息操作 trait
81pub trait GeoOperations {
82    /// 获取文件的 GPS 坐标
83    fn get_gps<P: AsRef<Path>>(&self, path: P) -> Result<Option<GpsCoordinate>>;
84
85    /// 设置文件的 GPS 坐标
86    fn set_gps<P: AsRef<Path>>(&self, path: P, coord: &GpsCoordinate) -> Result<()>;
87
88    /// 删除 GPS 信息
89    fn remove_gps<P: AsRef<Path>>(&self, path: P) -> Result<()>;
90
91    /// 从 GPS 轨迹文件地理标记
92    fn geotag_from_track<P: AsRef<Path>, Q: AsRef<Path>>(
93        &self,
94        image: P,
95        track_file: Q,
96    ) -> Result<()>;
97
98    /// 生成 GPS 轨迹文件
99    fn generate_tracklog<P: AsRef<Path>, Q: AsRef<Path>>(
100        &self,
101        images: &[P],
102        output: Q,
103    ) -> Result<()>;
104
105    /// 反向地理编码
106    fn reverse_geocode<P: AsRef<Path>>(&self, coord: &GpsCoordinate) -> Result<GeocodeResult>;
107}
108
109impl GeoOperations for ExifTool {
110    fn get_gps<P: AsRef<Path>>(&self, path: P) -> Result<Option<GpsCoordinate>> {
111        let metadata = self
112            .query(path)
113            .tag(TagId::GPS_LATITUDE.name())
114            .tag(TagId::GPS_LONGITUDE.name())
115            .execute()?;
116
117        let lat_val = metadata.get(TagId::GPS_LATITUDE.name());
118        let lon_val = metadata.get(TagId::GPS_LONGITUDE.name());
119
120        if let (Some(lat), Some(lon)) = (lat_val, lon_val) {
121            let lat_str = lat.to_string_lossy();
122            let lon_str = lon.to_string_lossy();
123
124            // 解析坐标值(简化版,实际需要更复杂的解析)
125            if let (Ok(lat_f), Ok(lon_f)) = (lat_str.parse::<f64>(), lon_str.parse::<f64>()) {
126                let coord = GpsCoordinate::new(lat_f, lon_f)?;
127                return Ok(Some(coord));
128            }
129        }
130
131        Ok(None)
132    }
133
134    fn set_gps<P: AsRef<Path>>(&self, path: P, coord: &GpsCoordinate) -> Result<()> {
135        let (lat, lon, alt) = coord.format();
136
137        let mut write = self.write(path);
138
139        write = write
140            .tag(TagId::GPS_LATITUDE.name(), &lat)
141            .tag(TagId::GPS_LONGITUDE.name(), &lon);
142
143        if let Some(altitude) = alt {
144            write = write.tag(TagId::GPS_ALTITUDE.name(), altitude);
145        }
146
147        write.overwrite_original(true).execute()?;
148
149        Ok(())
150    }
151
152    fn remove_gps<P: AsRef<Path>>(&self, path: P) -> Result<()> {
153        // 删除所有 GPS 相关标签
154        self.write(path)
155            .delete(TagId::GPS_LATITUDE.name())
156            .delete(TagId::GPS_LONGITUDE.name())
157            .delete(TagId::GPS_ALTITUDE.name())
158            .overwrite_original(true)
159            .execute()?;
160
161        Ok(())
162    }
163
164    fn geotag_from_track<P: AsRef<Path>, Q: AsRef<Path>>(
165        &self,
166        image: P,
167        track_file: Q,
168    ) -> Result<()> {
169        self.write(image)
170            .arg(format!("-geotag {}", track_file.as_ref().display()))
171            .overwrite_original(true)
172            .execute()?;
173
174        Ok(())
175    }
176
177    fn generate_tracklog<P: AsRef<Path>, Q: AsRef<Path>>(
178        &self,
179        images: &[P],
180        output: Q,
181    ) -> Result<()> {
182        // 收集所有文件的 GPS 数据
183        let mut track_points = Vec::new();
184
185        for image in images {
186            if let Some(coord) = self.get_gps(image)? {
187                let timestamp = self
188                    .read_tag::<String, _, _>(image, TagId::DATE_TIME_ORIGINAL.name())
189                    .ok();
190
191                track_points.push((coord, timestamp));
192            }
193        }
194
195        // 生成 GPX 格式的轨迹文件
196        use std::fs::File;
197        use std::io::Write;
198
199        let mut file = File::create(output)?;
200        writeln!(file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
201        writeln!(file, "<gpx version=\"1.1\">")?;
202        writeln!(file, "  <trk><trkseg>")?;
203
204        for (coord, _timestamp) in track_points {
205            writeln!(
206                file,
207                "    <trkpt lat=\"{}\" lon=\"{}\">",
208                coord.latitude, coord.longitude
209            )?;
210            if let Some(alt) = coord.altitude {
211                writeln!(file, "      <ele>{}</ele>", alt)?;
212            }
213            writeln!(file, "    </trkpt>")?;
214        }
215
216        writeln!(file, "  </trkseg></trk>")?;
217        writeln!(file, "</gpx>")?;
218
219        Ok(())
220    }
221
222    fn reverse_geocode<P: AsRef<Path>>(&self, coord: &GpsCoordinate) -> Result<GeocodeResult> {
223        // 使用 ExifTool 的 -geolocation 功能
224        // 需要提供地理编码数据库或调用外部服务
225
226        // 创建临时文件存储坐标
227        let mut temp_file = std::env::temp_dir();
228        temp_file.push(format!("geocode_{}.txt", std::process::id()));
229
230        // 写入坐标到临时文件
231        let coord_str = format!("{:.6},{:.6}", coord.latitude, coord.longitude);
232        std::fs::write(&temp_file, &coord_str).map_err(Error::Io)?;
233
234        // 使用 exiftool -geolocation 选项
235        // 注意:这需要系统安装了地理编码数据库
236        let args = vec![
237            "-geolocation".to_string(),
238            temp_file.to_string_lossy().to_string(),
239        ];
240
241        let response = self.execute_raw(&args)?;
242
243        // 清理临时文件
244        let _ = std::fs::remove_file(&temp_file);
245
246        // 解析响应
247        let output = response.text();
248        parse_geocode_result(&output)
249    }
250}
251
252/// 解析地理编码结果
253fn parse_geocode_result(output: &str) -> Result<GeocodeResult> {
254    let mut result = GeocodeResult {
255        city: None,
256        region: None,
257        country: None,
258        country_code: None,
259        address: None,
260    };
261
262    for line in output.lines() {
263        let line = line.trim();
264        if line.starts_with("City") {
265            result.city = extract_value(line);
266        } else if line.starts_with("Region") {
267            result.region = extract_value(line);
268        } else if line.starts_with("Country") {
269            result.country = extract_value(line);
270        } else if line.starts_with("Country Code") {
271            result.country_code = extract_value(line);
272        } else if line.starts_with("Address") {
273            result.address = extract_value(line);
274        }
275    }
276
277    Ok(result)
278}
279
280/// 从行中提取值
281fn extract_value(line: &str) -> Option<String> {
282    line.split_once(':')
283        .map(|(_, s)| s.trim().to_string())
284        .filter(|s| !s.is_empty())
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_gps_coordinate() {
293        let coord = GpsCoordinate::new(39.9042, 116.4074).unwrap();
294        assert_eq!(coord.latitude, 39.9042);
295        assert_eq!(coord.longitude, 116.4074);
296        assert_eq!(coord.altitude, None);
297
298        let coord = coord.with_altitude(50.0);
299        assert_eq!(coord.altitude, Some(50.0));
300    }
301
302    #[test]
303    fn test_gps_coordinate_format() {
304        let coord = GpsCoordinate::new(39.9042, 116.4074).unwrap();
305        let (lat, lon, alt) = coord.format();
306
307        assert!(lat.contains("N"));
308        assert!(lon.contains("E"));
309        assert_eq!(alt, None);
310    }
311
312    #[test]
313    fn test_gps_coordinate_validation() {
314        // 无效的纬度
315        assert!(GpsCoordinate::new(100.0, 0.0).is_err());
316        // 无效的经度
317        assert!(GpsCoordinate::new(0.0, 200.0).is_err());
318        // 有效坐标
319        assert!(GpsCoordinate::new(0.0, 0.0).is_ok());
320    }
321}