boring_parser/
coordinate.rs

1/// 緯度経度取得用のトレイト
2///
3/// 各バージョンのボーリングデータ構造体に実装され、
4/// 統一的なインターフェースで緯度経度情報を取得できます。
5
6/// 度分秒形式の座標
7#[derive(Debug, Clone, PartialEq)]
8pub struct DmsCoordinate {
9    /// 度
10    pub degree: Option<String>,
11    /// 分
12    pub minute: Option<String>,
13    /// 秒
14    pub second: Option<String>,
15}
16
17impl DmsCoordinate {
18    /// 度分秒を10進数(度)に変換
19    ///
20    /// 度、分、秒のいずれかがNoneまたはパースできない場合はNoneを返す
21    pub fn to_decimal(&self) -> Option<f64> {
22        let deg = self.degree.as_ref()?.parse::<f64>().ok()?;
23        let min = self.minute.as_ref()?.parse::<f64>().ok()?;
24        let sec = self.second.as_ref()?.parse::<f64>().ok()?;
25        Some(deg + min / 60.0 + sec / 3600.0)
26    }
27}
28
29/// 測地系の種類
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum GeodeticDatum {
32    /// 日本測地系(Tokyo Datum) - EPSG:4301
33    Tokyo,
34    /// 世界測地系2000(JGD2000) - EPSG:4612
35    Jgd2000,
36    /// 世界測地系2011(JGD2011) - EPSG:6668
37    Jgd2011,
38    /// WGS84 - EPSG:4326
39    Wgs84,
40    /// 不明な測地系
41    Unknown,
42}
43
44impl GeodeticDatum {
45    /// 測地系コードからGeodeticDatumに変換
46    ///
47    /// # コード対応表(DTD v4.00準拠)
48    /// - "00" → 日本測地系
49    /// - "01" → 世界測地系(JGD2000)
50    /// - "02" → 世界測地系(JGD2011)
51    pub fn from_code(s: &str) -> Self {
52        match s.trim() {
53            "00" => GeodeticDatum::Tokyo,
54            "01" => GeodeticDatum::Jgd2000,
55            "02" => GeodeticDatum::Jgd2011,
56            _ => GeodeticDatum::Unknown,
57        }
58    }
59
60    /// EPSGコードを取得
61    pub fn epsg_code(&self) -> Option<&'static str> {
62        match self {
63            GeodeticDatum::Tokyo => Some("EPSG:4301"),
64            GeodeticDatum::Jgd2000 => Some("EPSG:4612"),
65            GeodeticDatum::Jgd2011 => Some("EPSG:6668"),
66            GeodeticDatum::Wgs84 => Some("EPSG:4326"),
67            GeodeticDatum::Unknown => None,
68        }
69    }
70}
71
72/// 緯度経度情報
73#[derive(Debug, Clone, PartialEq)]
74pub struct GeoLocation {
75    /// 経度(度分秒)
76    pub longitude: DmsCoordinate,
77    /// 緯度(度分秒)
78    pub latitude: DmsCoordinate,
79    /// 測地系コード
80    pub geodetic_system: Option<String>,
81}
82
83impl GeoLocation {
84    /// WGS84座標系に変換した座標を取得
85    ///
86    /// # Returns
87    /// `Some((経度, 緯度))` - WGS84座標系での10進数座標
88    /// `None` - 座標が取得できない、測地系が不明、または変換に失敗した場合
89    pub fn to_wgs84(&self) -> Option<(f64, f64)> {
90        let lng = self.longitude.to_decimal()?;
91        let lat = self.latitude.to_decimal()?;
92        let datum = self
93            .geodetic_system
94            .as_ref()
95            .map(|s| GeodeticDatum::from_code(s))
96            .unwrap_or(GeodeticDatum::Unknown);
97        let from_crs = datum.epsg_code()?;
98        let proj = proj::Proj::new_known_crs(from_crs, "EPSG:4326", None).ok()?;
99        proj.convert((lng, lat)).ok()
100    }
101}
102
103/// WGS84座標を取得するためのトレイト
104pub trait GeoCoordinate {
105    /// WGS84座標系での (経度, 緯度) を取得
106    fn to_wgs84(&self) -> Option<(f64, f64)>;
107}
108
109impl GeoCoordinate for crate::boring_structs_400::Boring400 {
110    fn to_wgs84(&self) -> Option<(f64, f64)> {
111        let lng_lat = &self.title.longitude_latitude;
112        let loc = GeoLocation {
113            longitude: DmsCoordinate {
114                degree: lng_lat.longitude_degree.clone(),
115                minute: lng_lat.longitude_minute.clone(),
116                second: lng_lat.longitude_second.clone(),
117            },
118            latitude: DmsCoordinate {
119                degree: lng_lat.latitude_degree.clone(),
120                minute: lng_lat.latitude_minute.clone(),
121                second: lng_lat.latitude_second.clone(),
122            },
123            geodetic_system: lng_lat.geodetic_system.clone(),
124        };
125        loc.to_wgs84()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_dms_to_decimal() {
135        let dms = DmsCoordinate {
136            degree: Some("139".to_string()),
137            minute: Some("45".to_string()),
138            second: Some("0".to_string()),
139        };
140        let decimal = dms.to_decimal().unwrap();
141        assert!((decimal - 139.75).abs() < 0.0001);
142    }
143
144    #[test]
145    fn test_dms_to_decimal_with_seconds() {
146        // 東京: 139度41分30秒
147        let dms = DmsCoordinate {
148            degree: Some("139".to_string()),
149            minute: Some("41".to_string()),
150            second: Some("30".to_string()),
151        };
152        let decimal = dms.to_decimal().unwrap();
153        // 139 + 41/60 + 30/3600 = 139.69166...
154        assert!((decimal - 139.69166666).abs() < 0.0001);
155    }
156
157    #[test]
158    fn test_dms_to_decimal_none() {
159        let dms = DmsCoordinate {
160            degree: None,
161            minute: Some("45".to_string()),
162            second: Some("0".to_string()),
163        };
164        assert!(dms.to_decimal().is_none());
165    }
166
167    #[test]
168    fn test_geodetic_datum_from_code() {
169        assert_eq!(GeodeticDatum::from_code("00"), GeodeticDatum::Tokyo);
170        assert_eq!(GeodeticDatum::from_code("01"), GeodeticDatum::Jgd2000);
171        assert_eq!(GeodeticDatum::from_code("02"), GeodeticDatum::Jgd2011);
172        assert_eq!(GeodeticDatum::from_code("99"), GeodeticDatum::Unknown);
173    }
174
175    #[test]
176    fn test_to_wgs84_from_tokyo() {
177        // 日本測地系(コード: 00)での東京駅付近の座標
178        let loc = GeoLocation {
179            longitude: DmsCoordinate {
180                degree: Some("139".to_string()),
181                minute: Some("46".to_string()),
182                second: Some("0".to_string()),
183            },
184            latitude: DmsCoordinate {
185                degree: Some("35".to_string()),
186                minute: Some("40".to_string()),
187                second: Some("0".to_string()),
188            },
189            geodetic_system: Some("00".to_string()),
190        };
191
192        let (lng, lat) = loc.to_wgs84().unwrap();
193        // 日本測地系からWGS84への変換で、約10秒(約300m)程度のずれがある
194        assert!((lng - 139.76).abs() < 0.02);
195        assert!((lat - 35.67).abs() < 0.02);
196    }
197
198    #[test]
199    fn test_to_wgs84_from_jgd2000() {
200        // JGD2000(コード: 01)での座標はWGS84とほぼ同一
201        let loc = GeoLocation {
202            longitude: DmsCoordinate {
203                degree: Some("139".to_string()),
204                minute: Some("46".to_string()),
205                second: Some("0".to_string()),
206            },
207            latitude: DmsCoordinate {
208                degree: Some("35".to_string()),
209                minute: Some("40".to_string()),
210                second: Some("0".to_string()),
211            },
212            geodetic_system: Some("01".to_string()),
213        };
214
215        let (lng, lat) = loc.to_wgs84().unwrap();
216        // JGD2000はWGS84とほぼ同一なので、変換後も同じ値
217        assert!((lng - 139.76666666).abs() < 0.0001);
218        assert!((lat - 35.66666666).abs() < 0.0001);
219    }
220}