boring_parser/
coordinate.rs

1//! 緯度経度取得用のモジュール
2//!
3//! 各バージョンのボーリングデータ構造体に実装され、
4//! 統一的なインターフェースで緯度経度情報を取得できます。
5
6use crate::transform::JgdTransformer;
7
8/// 度分秒形式の座標
9#[derive(Debug, Clone, PartialEq)]
10pub struct DmsCoordinate {
11    /// 度
12    pub degree: Option<String>,
13    /// 分
14    pub minute: Option<String>,
15    /// 秒
16    pub second: Option<String>,
17}
18
19impl DmsCoordinate {
20    /// 度分秒を10進数(度)に変換
21    ///
22    /// 度、分、秒のいずれかがNoneまたはパースできない場合はNoneを返す
23    pub fn to_decimal(&self) -> Option<f64> {
24        let deg = self.degree.as_ref()?.parse::<f64>().ok()?;
25        let min = self.minute.as_ref()?.parse::<f64>().ok()?;
26        let sec = self.second.as_ref()?.parse::<f64>().ok()?;
27        Some(deg + min / 60.0 + sec / 3600.0)
28    }
29}
30
31/// 測地系の種類
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum GeodeticDatum {
34    /// 日本測地系(Tokyo Datum) - EPSG:4301
35    Tokyo,
36    /// 世界測地系2000(JGD2000) - EPSG:4612
37    Jgd2000,
38    /// 世界測地系2011(JGD2011) - EPSG:6668
39    Jgd2011,
40    /// WGS84 - EPSG:4326
41    Wgs84,
42    /// 不明な測地系
43    Unknown,
44}
45
46impl GeodeticDatum {
47    /// 測地系コードからGeodeticDatumに変換
48    ///
49    /// # コード対応表(DTD v4.00準拠)
50    /// - "00" → 日本測地系
51    /// - "01" → 世界測地系(JGD2000)
52    /// - "02" → 世界測地系(JGD2011)
53    pub fn from_code(s: &str) -> Self {
54        match s.trim() {
55            "00" => GeodeticDatum::Tokyo,
56            "01" => GeodeticDatum::Jgd2000,
57            "02" => GeodeticDatum::Jgd2011,
58            _ => GeodeticDatum::Unknown,
59        }
60    }
61
62    /// EPSGコードを取得
63    pub fn epsg_code(&self) -> Option<&'static str> {
64        match self {
65            GeodeticDatum::Tokyo => Some("EPSG:4301"),
66            GeodeticDatum::Jgd2000 => Some("EPSG:4612"),
67            GeodeticDatum::Jgd2011 => Some("EPSG:6668"),
68            GeodeticDatum::Wgs84 => Some("EPSG:4326"),
69            GeodeticDatum::Unknown => None,
70        }
71    }
72}
73
74/// 緯度経度情報
75#[derive(Debug, Clone, PartialEq)]
76pub struct GeoLocation {
77    /// 経度(度分秒)
78    pub longitude: DmsCoordinate,
79    /// 緯度(度分秒)
80    pub latitude: DmsCoordinate,
81    /// 測地系コード
82    pub geodetic_system: Option<String>,
83}
84
85impl GeoLocation {
86    /// WGS84座標系に変換した座標を取得
87    ///
88    /// 国土地理院のTKY2JGD/PatchJGDパラメータを使用して
89    /// Tokyo Datum/JGD2000 → JGD2011 → WGS84 の変換を行う。
90    ///
91    /// # Arguments
92    /// * `transformer` - JgdTransformer インスタンス
93    ///
94    /// # Returns
95    /// `Some((経度, 緯度))` - WGS84座標系での10進数座標
96    /// `None` - 座標が取得できない場合
97    ///
98    /// # Example
99    /// ```ignore
100    /// let transformer = JgdTransformer::new("TKY2JGD.par", "patch_files")?;
101    /// let (lng, lat) = location.to_wgs84(&transformer)?;
102    /// ```
103    pub fn to_wgs84(&self, transformer: &JgdTransformer) -> Option<(f64, f64)> {
104        let lng = self.longitude.to_decimal()?;
105        let lat = self.latitude.to_decimal()?;
106        let system_code = self.geodetic_system.as_deref().unwrap_or("02");
107
108        // Step 1: Tokyo/JGD2000 → JGD2011
109        let (lat_jgd2011, lng_jgd2011) =
110            transformer.transform_horizontal(lat, lng, system_code)?;
111
112        // Step 2: JGD2011 (EPSG:6668) → WGS84 (EPSG:4326)
113        let proj = proj::Proj::new_known_crs("EPSG:6668", "EPSG:4326", None).ok()?;
114        proj.convert((lng_jgd2011, lat_jgd2011)).ok()
115    }
116
117    /// 10進数座標を取得 (変換なし)
118    ///
119    /// # Returns
120    /// `Some((経度, 緯度))` - 10進数座標
121    /// `None` - 座標が取得できない場合
122    pub fn to_decimal(&self) -> Option<(f64, f64)> {
123        let lng = self.longitude.to_decimal()?;
124        let lat = self.latitude.to_decimal()?;
125        Some((lng, lat))
126    }
127
128    /// 測地系を取得
129    pub fn datum(&self) -> GeodeticDatum {
130        self.geodetic_system
131            .as_ref()
132            .map(|s| GeodeticDatum::from_code(s))
133            .unwrap_or(GeodeticDatum::Unknown)
134    }
135}
136
137/// 緯度経度情報を取得するためのトレイト
138pub trait GeoCoordinate {
139    /// GeoLocationを取得
140    ///
141    /// # Example
142    /// ```ignore
143    /// let loc = boring.geo_location();
144    /// let (lng, lat) = loc.to_jgd2011(&transformer)?;
145    /// let (lng, lat) = loc.to_wgs84()?;
146    /// ```
147    fn geo_location(&self) -> GeoLocation;
148}
149
150impl GeoCoordinate for crate::boring_structs_400::Boring400 {
151    fn geo_location(&self) -> GeoLocation {
152        let lng_lat = &self.title.longitude_latitude;
153        GeoLocation {
154            longitude: DmsCoordinate {
155                degree: lng_lat.longitude_degree.clone(),
156                minute: lng_lat.longitude_minute.clone(),
157                second: lng_lat.longitude_second.clone(),
158            },
159            latitude: DmsCoordinate {
160                degree: lng_lat.latitude_degree.clone(),
161                minute: lng_lat.latitude_minute.clone(),
162                second: lng_lat.latitude_second.clone(),
163            },
164            geodetic_system: lng_lat.geodetic_system.clone(),
165        }
166    }
167}
168
169impl GeoCoordinate for crate::boring_structs_300::Boring300 {
170    fn geo_location(&self) -> GeoLocation {
171        let lng_lat = &self.title.longitude_latitude;
172        GeoLocation {
173            longitude: DmsCoordinate {
174                degree: lng_lat.longitude_degree.clone(),
175                minute: lng_lat.longitude_minute.clone(),
176                second: lng_lat.longitude_second.clone(),
177            },
178            latitude: DmsCoordinate {
179                degree: lng_lat.latitude_degree.clone(),
180                minute: lng_lat.latitude_minute.clone(),
181                second: lng_lat.latitude_second.clone(),
182            },
183            geodetic_system: lng_lat.geodetic_system.clone(),
184        }
185    }
186}
187
188impl GeoCoordinate for crate::boring_structs_210::Boring210 {
189    fn geo_location(&self) -> GeoLocation {
190        let lng_lat = &self.title.longitude_latitude;
191        GeoLocation {
192            longitude: DmsCoordinate {
193                degree: lng_lat.longitude_degree.clone(),
194                minute: lng_lat.longitude_minute.clone(),
195                second: lng_lat.longitude_second.clone(),
196            },
197            latitude: DmsCoordinate {
198                degree: lng_lat.latitude_degree.clone(),
199                minute: lng_lat.latitude_minute.clone(),
200                second: lng_lat.latitude_second.clone(),
201            },
202            geodetic_system: lng_lat.geodetic_system.clone(),
203        }
204    }
205}
206
207impl GeoCoordinate for crate::boring_structs_110::Boring110 {
208    fn geo_location(&self) -> GeoLocation {
209        let lng_lat = &self.title.longitude_latitude;
210        GeoLocation {
211            longitude: DmsCoordinate {
212                degree: lng_lat.longitude_degree.clone(),
213                minute: lng_lat.longitude_minute.clone(),
214                second: lng_lat.longitude_second.clone(),
215            },
216            latitude: DmsCoordinate {
217                degree: lng_lat.latitude_degree.clone(),
218                minute: lng_lat.latitude_minute.clone(),
219                second: lng_lat.latitude_second.clone(),
220            },
221            geodetic_system: lng_lat.geodetic_system.clone(),
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_dms_to_decimal() {
232        let dms = DmsCoordinate {
233            degree: Some("139".to_string()),
234            minute: Some("45".to_string()),
235            second: Some("0".to_string()),
236        };
237        let decimal = dms.to_decimal().unwrap();
238        assert!((decimal - 139.75).abs() < 0.0001);
239    }
240
241    #[test]
242    fn test_dms_to_decimal_with_seconds() {
243        // 東京: 139度41分30秒
244        let dms = DmsCoordinate {
245            degree: Some("139".to_string()),
246            minute: Some("41".to_string()),
247            second: Some("30".to_string()),
248        };
249        let decimal = dms.to_decimal().unwrap();
250        // 139 + 41/60 + 30/3600 = 139.69166...
251        assert!((decimal - 139.69166666).abs() < 0.0001);
252    }
253
254    #[test]
255    fn test_dms_to_decimal_none() {
256        let dms = DmsCoordinate {
257            degree: None,
258            minute: Some("45".to_string()),
259            second: Some("0".to_string()),
260        };
261        assert!(dms.to_decimal().is_none());
262    }
263
264    #[test]
265    fn test_geodetic_datum_from_code() {
266        assert_eq!(GeodeticDatum::from_code("00"), GeodeticDatum::Tokyo);
267        assert_eq!(GeodeticDatum::from_code("01"), GeodeticDatum::Jgd2000);
268        assert_eq!(GeodeticDatum::from_code("02"), GeodeticDatum::Jgd2011);
269        assert_eq!(GeodeticDatum::from_code("99"), GeodeticDatum::Unknown);
270    }
271
272    #[test]
273    fn test_to_wgs84() {
274        // パラメータファイルが存在する場合のみテスト
275        let workspace_root =
276            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
277        let tky_path = workspace_root.join("params/TKY2JGD.par");
278        let patch_dir = workspace_root.join("params/patch_files");
279
280        if !tky_path.exists() || !patch_dir.exists() {
281            println!("Skipping test: params not found");
282            return;
283        }
284
285        let transformer = crate::transform::JgdTransformer::new(&tky_path, &patch_dir)
286            .expect("Failed to create transformer");
287
288        // 日本測地系(コード: 00)での東京駅付近の座標
289        let loc = GeoLocation {
290            longitude: DmsCoordinate {
291                degree: Some("139".to_string()),
292                minute: Some("46".to_string()),
293                second: Some("0".to_string()),
294            },
295            latitude: DmsCoordinate {
296                degree: Some("35".to_string()),
297                minute: Some("40".to_string()),
298                second: Some("0".to_string()),
299            },
300            geodetic_system: Some("00".to_string()),
301        };
302
303        let (lng, lat) = loc.to_wgs84(&transformer).unwrap();
304        // 日本測地系からWGS84への変換で、約10秒(約300m)程度のずれがある
305        assert!((lng - 139.76).abs() < 0.02);
306        assert!((lat - 35.67).abs() < 0.02);
307
308        println!("Tokyo Datum → WGS84: ({}, {})", lng, lat);
309    }
310}