Skip to main content

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        // Tokyo/JGD2000 → JGD2011 → WGS84 (キャッシュ済みprojを使用)
109        transformer.to_wgs84(lat, lng, system_code)
110    }
111
112    /// 10進数座標を取得 (変換なし)
113    ///
114    /// # Returns
115    /// `Some((経度, 緯度))` - 10進数座標
116    /// `None` - 座標が取得できない場合
117    pub fn to_decimal(&self) -> Option<(f64, f64)> {
118        let lng = self.longitude.to_decimal()?;
119        let lat = self.latitude.to_decimal()?;
120        Some((lng, lat))
121    }
122
123    /// 測地系を取得
124    pub fn datum(&self) -> GeodeticDatum {
125        self.geodetic_system
126            .as_ref()
127            .map(|s| GeodeticDatum::from_code(s))
128            .unwrap_or(GeodeticDatum::Unknown)
129    }
130}
131
132/// 緯度経度情報を取得するためのトレイト
133pub trait GeoCoordinate {
134    /// GeoLocationを取得
135    ///
136    /// # Example
137    /// ```ignore
138    /// let loc = boring.geo_location();
139    /// let (lng, lat) = loc.to_jgd2011(&transformer)?;
140    /// let (lng, lat) = loc.to_wgs84()?;
141    /// ```
142    fn geo_location(&self) -> GeoLocation;
143}
144
145impl GeoCoordinate for crate::boring_structs_400::Boring400 {
146    fn geo_location(&self) -> GeoLocation {
147        let lng_lat = &self.title.longitude_latitude;
148        GeoLocation {
149            longitude: DmsCoordinate {
150                degree: lng_lat.longitude_degree.clone(),
151                minute: lng_lat.longitude_minute.clone(),
152                second: lng_lat.longitude_second.clone(),
153            },
154            latitude: DmsCoordinate {
155                degree: lng_lat.latitude_degree.clone(),
156                minute: lng_lat.latitude_minute.clone(),
157                second: lng_lat.latitude_second.clone(),
158            },
159            geodetic_system: lng_lat.geodetic_system.clone(),
160        }
161    }
162}
163
164impl GeoCoordinate for crate::boring_structs_300::Boring300 {
165    fn geo_location(&self) -> GeoLocation {
166        let lng_lat = &self.title.longitude_latitude;
167        GeoLocation {
168            longitude: DmsCoordinate {
169                degree: lng_lat.longitude_degree.clone(),
170                minute: lng_lat.longitude_minute.clone(),
171                second: lng_lat.longitude_second.clone(),
172            },
173            latitude: DmsCoordinate {
174                degree: lng_lat.latitude_degree.clone(),
175                minute: lng_lat.latitude_minute.clone(),
176                second: lng_lat.latitude_second.clone(),
177            },
178            geodetic_system: lng_lat.geodetic_system.clone(),
179        }
180    }
181}
182
183impl GeoCoordinate for crate::boring_structs_200::Boring200 {
184    fn geo_location(&self) -> GeoLocation {
185        let lng_lat = &self.title.longitude_latitude;
186        GeoLocation {
187            longitude: DmsCoordinate {
188                degree: lng_lat.longitude_degree.clone(),
189                minute: lng_lat.longitude_minute.clone(),
190                second: lng_lat.longitude_second.clone(),
191            },
192            latitude: DmsCoordinate {
193                degree: lng_lat.latitude_degree.clone(),
194                minute: lng_lat.latitude_minute.clone(),
195                second: lng_lat.latitude_second.clone(),
196            },
197            geodetic_system: lng_lat.geodetic_system.clone(),
198        }
199    }
200}
201
202impl GeoCoordinate for crate::boring_structs_210::Boring210 {
203    fn geo_location(&self) -> GeoLocation {
204        let lng_lat = &self.title.longitude_latitude;
205        GeoLocation {
206            longitude: DmsCoordinate {
207                degree: lng_lat.longitude_degree.clone(),
208                minute: lng_lat.longitude_minute.clone(),
209                second: lng_lat.longitude_second.clone(),
210            },
211            latitude: DmsCoordinate {
212                degree: lng_lat.latitude_degree.clone(),
213                minute: lng_lat.latitude_minute.clone(),
214                second: lng_lat.latitude_second.clone(),
215            },
216            geodetic_system: lng_lat.geodetic_system.clone(),
217        }
218    }
219}
220
221impl GeoCoordinate for crate::boring_structs_110::Boring110 {
222    fn geo_location(&self) -> GeoLocation {
223        let lng_lat = &self.title.longitude_latitude;
224        GeoLocation {
225            longitude: DmsCoordinate {
226                degree: lng_lat.longitude_degree.clone(),
227                minute: lng_lat.longitude_minute.clone(),
228                second: lng_lat.longitude_second.clone(),
229            },
230            latitude: DmsCoordinate {
231                degree: lng_lat.latitude_degree.clone(),
232                minute: lng_lat.latitude_minute.clone(),
233                second: lng_lat.latitude_second.clone(),
234            },
235            geodetic_system: lng_lat.geodetic_system.clone(),
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_dms_to_decimal() {
246        let dms = DmsCoordinate {
247            degree: Some("139".to_string()),
248            minute: Some("45".to_string()),
249            second: Some("0".to_string()),
250        };
251        let decimal = dms.to_decimal().unwrap();
252        assert!((decimal - 139.75).abs() < 0.0001);
253    }
254
255    #[test]
256    fn test_dms_to_decimal_with_seconds() {
257        // 東京: 139度41分30秒
258        let dms = DmsCoordinate {
259            degree: Some("139".to_string()),
260            minute: Some("41".to_string()),
261            second: Some("30".to_string()),
262        };
263        let decimal = dms.to_decimal().unwrap();
264        // 139 + 41/60 + 30/3600 = 139.69166...
265        assert!((decimal - 139.69166666).abs() < 0.0001);
266    }
267
268    #[test]
269    fn test_dms_to_decimal_none() {
270        let dms = DmsCoordinate {
271            degree: None,
272            minute: Some("45".to_string()),
273            second: Some("0".to_string()),
274        };
275        assert!(dms.to_decimal().is_none());
276    }
277
278    #[test]
279    fn test_geodetic_datum_from_code() {
280        assert_eq!(GeodeticDatum::from_code("00"), GeodeticDatum::Tokyo);
281        assert_eq!(GeodeticDatum::from_code("01"), GeodeticDatum::Jgd2000);
282        assert_eq!(GeodeticDatum::from_code("02"), GeodeticDatum::Jgd2011);
283        assert_eq!(GeodeticDatum::from_code("99"), GeodeticDatum::Unknown);
284    }
285
286    #[test]
287    fn test_to_wgs84() {
288        // パラメータファイルが存在する場合のみテスト
289        let workspace_root =
290            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
291        let tky_path = workspace_root.join("params/TKY2JGD.par");
292        let patch_dir = workspace_root.join("params/patch_files");
293
294        if !tky_path.exists() || !patch_dir.exists() {
295            println!("Skipping test: params not found");
296            return;
297        }
298
299        let transformer = crate::transform::JgdTransformer::new(&tky_path, &patch_dir)
300            .expect("Failed to create transformer");
301
302        // 日本測地系(コード: 00)での東京駅付近の座標
303        let loc = GeoLocation {
304            longitude: DmsCoordinate {
305                degree: Some("139".to_string()),
306                minute: Some("46".to_string()),
307                second: Some("0".to_string()),
308            },
309            latitude: DmsCoordinate {
310                degree: Some("35".to_string()),
311                minute: Some("40".to_string()),
312                second: Some("0".to_string()),
313            },
314            geodetic_system: Some("00".to_string()),
315        };
316
317        let (lng, lat) = loc.to_wgs84(&transformer).unwrap();
318        // 日本測地系からWGS84への変換で、約10秒(約300m)程度のずれがある
319        assert!((lng - 139.76).abs() < 0.02);
320        assert!((lat - 35.67).abs() < 0.02);
321
322        println!("Tokyo Datum → WGS84: ({}, {})", lng, lat);
323    }
324}