Skip to main content

boring_parser/
transform.rs

1//! 測地系変換モジュール
2//!
3//! 日本測地系(Tokyo Datum)およびJGD2000からJGD2011への座標変換を提供する。
4//! 国土地理院のパラメータファイル(TKY2JGD, PatchJGD)を使用。
5//!
6//! # 地震補正の適用ルール
7//! 地震による地殻変動の補正は、**調査日が地震発生日より前**の場合のみ適用される。
8//! 調査日が地震後の場合、測量時点で既に地殻変動後の位置が記録されているため、
9//! 補正を適用すると誤った位置になる。
10
11use encoding_rs::SHIFT_JIS;
12use jgdtrans::{Format, ParData, Parameter, Point, Transformer};
13use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16use thiserror::Error;
17
18/// 変換エラー
19#[derive(Error, Debug)]
20pub enum TransformError {
21    #[error("パラメータファイル読み込みエラー: {0}")]
22    FileRead(#[from] std::io::Error),
23
24    #[error("パラメータパースエラー: {0}")]
25    Parse(String),
26
27    #[error("変換エラー: {0}")]
28    Transform(String),
29
30    #[error("パラメータディレクトリが存在しません: {0}")]
31    DirectoryNotFound(String),
32}
33
34/// 調査開始日(年月日)
35///
36/// 地震補正の適用判定に使用。調査開始日が地震発生日より前の場合のみ補正を適用する。
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38pub struct SurveyStartDate {
39    pub year: i32,
40    pub month: u32,
41    pub day: u32,
42}
43
44impl SurveyStartDate {
45    pub fn new(year: i32, month: u32, day: u32) -> Self {
46        Self { year, month, day }
47    }
48
49    /// YYYYMMDD形式の文字列からパース
50    pub fn from_yyyymmdd(s: &str) -> Option<Self> {
51        if s.len() < 8 {
52            return None;
53        }
54        let year = s[0..4].parse().ok()?;
55        let month = s[4..6].parse().ok()?;
56        let day = s[6..8].parse().ok()?;
57        Some(Self { year, month, day })
58    }
59
60    /// 年月日を個別にパース
61    pub fn from_parts(year: Option<&str>, month: Option<&str>, day: Option<&str>) -> Option<Self> {
62        Some(Self {
63            year: year?.parse().ok()?,
64            month: month?.parse().ok()?,
65            day: day?.parse().ok()?,
66        })
67    }
68}
69
70/// 地震情報
71struct EarthquakeInfo {
72    /// ファイル名に含まれるパターン
73    file_pattern: &'static str,
74    /// 地震発生日
75    date: SurveyStartDate,
76}
77
78/// 地震補正パラメータの一覧(発生日順)
79/// 調査日がこの日付より前の場合のみ、対応するパラメータを適用する
80const EARTHQUAKES: &[EarthquakeInfo] = &[
81    EarthquakeInfo {
82        file_pattern: "tokachi2003",
83        date: SurveyStartDate { year: 2003, month: 9, day: 26 },
84    },
85    EarthquakeInfo {
86        file_pattern: "fukuoka2005",
87        date: SurveyStartDate { year: 2005, month: 3, day: 20 },
88    },
89    EarthquakeInfo {
90        file_pattern: "noto2007",
91        date: SurveyStartDate { year: 2007, month: 3, day: 25 },
92    },
93    EarthquakeInfo {
94        file_pattern: "chuetsuoki2007",
95        date: SurveyStartDate { year: 2007, month: 7, day: 16 },
96    },
97    EarthquakeInfo {
98        file_pattern: "iwatemiyagi2008",
99        date: SurveyStartDate { year: 2008, month: 6, day: 14 },
100    },
101    EarthquakeInfo {
102        file_pattern: "touhokutaiheiyouoki2011",
103        date: SurveyStartDate { year: 2011, month: 3, day: 11 },
104    },
105    EarthquakeInfo {
106        file_pattern: "kumamoto2016",
107        date: SurveyStartDate { year: 2016, month: 4, day: 14 },
108    },
109];
110
111/// ファイル名が地震補正パラメータかどうかを判定し、適用すべきかを返す
112fn should_apply_earthquake_correction(file_name: &str, survey_date: Option<SurveyStartDate>) -> bool {
113    // 地震補正ファイルでない場合は常に適用(ジオイド補正など)
114    let earthquake = EARTHQUAKES.iter().find(|eq| file_name.contains(eq.file_pattern));
115
116    match (earthquake, survey_date) {
117        // 地震ファイルで、調査日が指定されている場合
118        (Some(eq), Some(date)) => {
119            // 調査日が地震発生日より前の場合のみ適用
120            date < eq.date
121        }
122        // 地震ファイルだが調査日が不明 → 安全のため適用しない
123        (Some(_), None) => false,
124        // 地震ファイルでない(ジオイド等)→ 常に適用
125        (None, _) => true,
126    }
127}
128
129/// パラメータファイルを読み込む(Shift_JIS/UTF-8自動判定)
130pub fn read_param_file<P: AsRef<Path>>(path: P) -> Result<String, std::io::Error> {
131    let bytes = fs::read(&path)?;
132
133    // まずUTF-8として試す
134    if let Ok(s) = String::from_utf8(bytes.clone()) {
135        return Ok(s);
136    }
137
138    // UTF-8でなければShift_JISとして変換
139    let (decoded, _, _) = SHIFT_JIS.decode(&bytes);
140    Ok(decoded.into_owned())
141}
142
143/// JGD2011座標変換器
144///
145/// Tokyo Datum/JGD2000からJGD2011への変換を行う。
146/// - Step 1: Tokyo Datum → JGD2000 (TKY2JGD)
147/// - Step 2: JGD2000 → JGD2011 (PatchJGD統合)
148/// - Step 3: JGD2011 → WGS84 (proj)
149pub struct JgdTransformer {
150    /// Tokyo Datum → JGD2000 変換器
151    tky_tf: Transformer<ParData>,
152    /// JGD2000 → JGD2011 統合変換器 (水平+垂直)
153    unified_tf: Transformer<ParData>,
154    /// JGD2011 → WGS84 変換器 (キャッシュ)
155    jgd2011_to_wgs84: proj::Proj,
156}
157
158impl JgdTransformer {
159    /// パラメータファイルを読み込んで変換器を初期化(調査日指定あり)
160    ///
161    /// # Arguments
162    /// * `tky_path` - TKY2JGD.par のパス
163    /// * `patch_dir` - PatchJGD系ファイルが入ったディレクトリのパス
164    /// * `survey_date` - 調査日(地震補正の適用判定に使用)
165    ///
166    /// # 地震補正の適用ルール
167    /// - 調査日が地震発生日より**前**の場合のみ、その地震の補正を適用
168    /// - 調査日が地震発生日より**後**の場合、補正は適用しない(既に変動後の位置)
169    /// - ジオイド補正(hyokorev2014_geoid2011_h.par)は常に適用
170    ///
171    /// # Example
172    /// ```ignore
173    /// let survey_date = SurveyStartDate::new(2010, 5, 15);
174    /// let transformer = JgdTransformer::with_survey_date(
175    ///     "params/TKY2JGD.par",
176    ///     "params/patch_files",
177    ///     survey_date
178    /// )?;
179    /// ```
180    pub fn with_survey_date<P: AsRef<Path>>(
181        tky_path: P,
182        patch_dir: P,
183        survey_date: SurveyStartDate,
184    ) -> Result<Self, TransformError> {
185        let tky_content = read_param_file(&tky_path)?;
186        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
187            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
188
189        let unified_tf = Self::create_unified_transformer(patch_dir, Some(survey_date))?;
190
191        let jgd2011_to_wgs84 = proj::Proj::new_known_crs("EPSG:6668", "EPSG:4326", None)
192            .map_err(|e| TransformError::Transform(format!("proj init error: {}", e)))?;
193
194        Ok(Self { tky_tf, unified_tf, jgd2011_to_wgs84 })
195    }
196
197    /// パラメータファイルを読み込んで変換器を初期化(調査日なし)
198    ///
199    /// **注意**: 調査日が不明な場合、地震補正は適用されません。
200    /// ジオイド補正のみが適用されます。
201    ///
202    /// # Arguments
203    /// * `tky_path` - TKY2JGD.par のパス
204    /// * `patch_dir` - PatchJGD系ファイルが入ったディレクトリのパス
205    pub fn new<P: AsRef<Path>>(tky_path: P, patch_dir: P) -> Result<Self, TransformError> {
206        let tky_content = read_param_file(&tky_path)?;
207        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
208            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
209
210        // 調査日なしの場合、地震補正は適用しない(ジオイド補正のみ)
211        let unified_tf = Self::create_unified_transformer(patch_dir, None)?;
212
213        let jgd2011_to_wgs84 = proj::Proj::new_known_crs("EPSG:6668", "EPSG:4326", None)
214            .map_err(|e| TransformError::Transform(format!("proj init error: {}", e)))?;
215
216        Ok(Self { tky_tf, unified_tf, jgd2011_to_wgs84 })
217    }
218
219    /// TKY2JGDのみで変換器を初期化 (PatchJGDファイルなし)
220    ///
221    /// JGD2000→JGD2011変換が不要な場合に使用
222    pub fn new_tky_only<P: AsRef<Path>>(tky_path: P) -> Result<Self, TransformError> {
223        let tky_content = read_param_file(&tky_path)?;
224        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
225            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
226
227        // 空の統合変換器を作成
228        let empty_params: HashMap<u32, Parameter> = HashMap::new();
229        let empty_data =
230            ParData::with_description(Format::PatchJGD_HV, empty_params, "Empty".to_string());
231        let unified_tf = Transformer::new(empty_data);
232
233        let jgd2011_to_wgs84 = proj::Proj::new_known_crs("EPSG:6668", "EPSG:4326", None)
234            .map_err(|e| TransformError::Transform(format!("proj init error: {}", e)))?;
235
236        Ok(Self { tky_tf, unified_tf, jgd2011_to_wgs84 })
237    }
238
239    /// ディレクトリ内のパラメータファイルを加算マージして1つのTransformerを作る
240    ///
241    /// # Directory Structure
242    /// ```text
243    /// patch_dir/
244    /// ├── horizontal/    # 水平補正ファイル (PatchJGD形式)
245    /// │   ├── touhokutaiheiyouoki2011.par
246    /// │   └── ...
247    /// └── elevation/     # 標高補正ファイル (PatchJGD_H/HyokoRev形式)
248    ///     ├── touhokutaiheiyouoki2011_h.par
249    ///     ├── hyokorev2014_geoid2011_h.par
250    ///     └── ...
251    /// ```
252    ///
253    /// # Arguments
254    /// * `dir_path` - パラメータファイルのルートディレクトリ
255    /// * `survey_date` - 調査日(Noneの場合、地震補正は適用しない)
256    fn create_unified_transformer<P: AsRef<Path>>(
257        dir_path: P,
258        survey_date: Option<SurveyStartDate>,
259    ) -> Result<Transformer<ParData>, TransformError> {
260        let dir = dir_path.as_ref();
261
262        // ディレクトリが存在しない場合は空のTransformerを返す
263        if !dir.exists() {
264            let empty_params: HashMap<u32, Parameter> = HashMap::new();
265            let empty_data = ParData::with_description(
266                Format::PatchJGD_HV,
267                empty_params,
268                "No patch files".to_string(),
269            );
270            return Ok(Transformer::new(empty_data));
271        }
272
273        let mut unified_params: HashMap<u32, Parameter> = HashMap::new();
274        let mut description = String::from("Unified Parameter Set:\n");
275        if let Some(date) = survey_date {
276            description.push_str(&format!(
277                "Survey date: {}/{}/{}\n",
278                date.year, date.month, date.day
279            ));
280        } else {
281            description.push_str("Survey date: unknown (earthquake corrections disabled)\n");
282        }
283        let mut loaded_count = 0;
284
285        // horizontal/ と elevation/ サブディレクトリをスキャン
286        let horizontal_dir = dir.join("horizontal");
287        let elevation_dir = dir.join("elevation");
288
289        // 水平補正ファイルを読み込み
290        if horizontal_dir.exists() {
291            description.push_str("\n[Horizontal corrections]\n");
292            loaded_count += Self::load_param_files(
293                &horizontal_dir,
294                Format::PatchJGD,
295                survey_date,
296                &mut unified_params,
297                &mut description,
298            )?;
299        }
300
301        // 標高補正ファイルを読み込み
302        if elevation_dir.exists() {
303            description.push_str("\n[Elevation corrections]\n");
304            loaded_count += Self::load_param_files_elevation(
305                &elevation_dir,
306                survey_date,
307                &mut unified_params,
308                &mut description,
309            )?;
310        }
311
312        if loaded_count == 0 {
313            description = "No patch files loaded".to_string();
314        }
315
316        let unified_data =
317            ParData::with_description(Format::PatchJGD_HV, unified_params, description);
318        Ok(Transformer::new(unified_data))
319    }
320
321    /// 指定ディレクトリからパラメータファイルを読み込む(水平用)
322    fn load_param_files(
323        dir: &Path,
324        format: Format,
325        survey_date: Option<SurveyStartDate>,
326        unified_params: &mut HashMap<u32, Parameter>,
327        description: &mut String,
328    ) -> Result<usize, TransformError> {
329        let mut loaded_count = 0;
330
331        for entry in fs::read_dir(dir)? {
332            let entry = entry?;
333            let path = entry.path();
334
335            if path.extension().and_then(|s| s.to_str()) != Some("par") {
336                continue;
337            }
338
339            let file_name = match path.file_name().and_then(|s| s.to_str()) {
340                Some(name) => name.to_string(),
341                None => continue,
342            };
343
344            // 地震補正の適用判定
345            if !should_apply_earthquake_correction(&file_name, survey_date) {
346                description.push_str(&format!("  - {} (skipped: survey after earthquake)\n", file_name));
347                continue;
348            }
349
350            let content = read_param_file(&path)?;
351            let par_data = ParData::from_str(&content, format)
352                .map_err(|e| TransformError::Parse(format!("{}: {}", file_name, e)))?;
353
354            for (mesh_code, param) in par_data.parameter {
355                unified_params
356                    .entry(mesh_code)
357                    .and_modify(|existing| {
358                        existing.latitude += param.latitude;
359                        existing.longitude += param.longitude;
360                        existing.altitude += param.altitude;
361                    })
362                    .or_insert(param);
363            }
364
365            description.push_str(&format!("  - {} (applied)\n", file_name));
366            loaded_count += 1;
367        }
368
369        Ok(loaded_count)
370    }
371
372    /// 指定ディレクトリからパラメータファイルを読み込む(標高用)
373    fn load_param_files_elevation(
374        dir: &Path,
375        survey_date: Option<SurveyStartDate>,
376        unified_params: &mut HashMap<u32, Parameter>,
377        description: &mut String,
378    ) -> Result<usize, TransformError> {
379        let mut loaded_count = 0;
380
381        for entry in fs::read_dir(dir)? {
382            let entry = entry?;
383            let path = entry.path();
384
385            if path.extension().and_then(|s| s.to_str()) != Some("par") {
386                continue;
387            }
388
389            let file_name = match path.file_name().and_then(|s| s.to_str()) {
390                Some(name) => name.to_string(),
391                None => continue,
392            };
393
394            // 地震補正の適用判定
395            if !should_apply_earthquake_correction(&file_name, survey_date) {
396                description.push_str(&format!("  - {} (skipped: survey after earthquake)\n", file_name));
397                continue;
398            }
399
400            // ファイル名でフォーマットを判別
401            let format = if file_name.contains("hyoko") {
402                Format::HyokoRev // ジオイドモデル改定
403            } else {
404                Format::PatchJGD_H // 地震(標高)
405            };
406
407            let content = read_param_file(&path)?;
408            let par_data = ParData::from_str(&content, format)
409                .map_err(|e| TransformError::Parse(format!("{}: {}", file_name, e)))?;
410
411            for (mesh_code, param) in par_data.parameter {
412                unified_params
413                    .entry(mesh_code)
414                    .and_modify(|existing| {
415                        existing.latitude += param.latitude;
416                        existing.longitude += param.longitude;
417                        existing.altitude += param.altitude;
418                    })
419                    .or_insert(param);
420            }
421
422            description.push_str(&format!("  - {} (applied)\n", file_name));
423            loaded_count += 1;
424        }
425
426        Ok(loaded_count)
427    }
428
429    /// 座標をJGD2011に変換 (水平座標のみ)
430    ///
431    /// # Arguments
432    /// * `lat` - 緯度 (度, 10進数)
433    /// * `lon` - 経度 (度, 10進数)
434    /// * `system_code` - 測地系コード ("00"=Tokyo, "01"=JGD2000, "02"=JGD2011)
435    ///
436    /// # Returns
437    /// 変換後の (緯度, 経度) または変換失敗時にNone
438    pub fn transform_horizontal(
439        &self,
440        lat: f64,
441        lon: f64,
442        system_code: &str,
443    ) -> Option<(f64, f64)> {
444        let result = self.transform_point(lat, lon, 0.0, system_code).ok()?;
445        Some((result.latitude, result.longitude))
446    }
447
448    /// 座標をJGD2011に変換 (水平+垂直)
449    ///
450    /// # Arguments
451    /// * `lat` - 緯度 (度, 10進数)
452    /// * `lon` - 経度 (度, 10進数)
453    /// * `alt` - 標高 (m)
454    /// * `system_code` - 測地系コード ("00"=Tokyo, "01"=JGD2000, "02"=JGD2011)
455    ///
456    /// # Returns
457    /// 変換後の (緯度, 経度, 標高)
458    pub fn transform_full(
459        &self,
460        lat: f64,
461        lon: f64,
462        alt: f64,
463        system_code: &str,
464    ) -> Result<(f64, f64, f64), TransformError> {
465        let result = self.transform_point(lat, lon, alt, system_code)?;
466        Ok((result.latitude, result.longitude, result.altitude))
467    }
468
469    /// 座標をWGS84に変換 (水平座標のみ)
470    ///
471    /// Tokyo Datum/JGD2000 → JGD2011 → WGS84 の変換を行う。
472    ///
473    /// # Arguments
474    /// * `lat` - 緯度 (度, 10進数)
475    /// * `lon` - 経度 (度, 10進数)
476    /// * `system_code` - 測地系コード ("00"=Tokyo, "01"=JGD2000, "02"=JGD2011)
477    ///
478    /// # Returns
479    /// 変換後の (経度, 緯度) - WGS84座標
480    pub fn to_wgs84(
481        &self,
482        lat: f64,
483        lon: f64,
484        system_code: &str,
485    ) -> Option<(f64, f64)> {
486        // Step 1-2: Tokyo/JGD2000 → JGD2011
487        let (lat_jgd2011, lng_jgd2011) = self.transform_horizontal(lat, lon, system_code)?;
488
489        // Step 3: JGD2011 → WGS84 (キャッシュ済みprojを使用)
490        self.jgd2011_to_wgs84.convert((lng_jgd2011, lat_jgd2011)).ok()
491    }
492
493    /// 内部変換処理
494    fn transform_point(
495        &self,
496        lat: f64,
497        lon: f64,
498        alt: f64,
499        system_code: &str,
500    ) -> Result<Point, TransformError> {
501        let input_point = Point::new_unchecked(lat, lon, alt);
502
503        // コードの正規化 (先頭のゼロ除去)
504        let code = system_code.trim().trim_start_matches('0');
505
506        match code {
507            // Tokyo Datum (日本測地系) → Step 1 + Step 2
508            // code "00" の場合、trim_start_matches('0') で "" になる
509            "" | "0" => {
510                // Step 1: Tokyo → JGD2000
511                let corr_tky = self
512                    .tky_tf
513                    .forward_corr(&input_point)
514                    .map_err(|e| TransformError::Transform(format!("TKY2JGD: {}", e)))?;
515                let p_2000 = &input_point + corr_tky;
516
517                // Step 2: JGD2000 → JGD2011
518                match self.unified_tf.forward_corr(&p_2000) {
519                    Ok(corr_unified) => Ok(&p_2000 + corr_unified),
520                    Err(_) => Ok(p_2000), // パッチ範囲外の場合はJGD2000のまま
521                }
522            }
523
524            // JGD2000 (世界測地系2000) → Step 2のみ
525            "1" => match self.unified_tf.forward_corr(&input_point) {
526                Ok(corr_unified) => Ok(&input_point + corr_unified),
527                Err(_) => Ok(input_point), // パッチ範囲外の場合はそのまま
528            },
529
530            // JGD2011 → 変換不要
531            "2" => Ok(input_point),
532
533            // 未知のコード → そのまま返す
534            _ => Ok(input_point),
535        }
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[test]
544    fn test_code_normalization() {
545        // "00" → "" (Tokyo Datum)
546        let code = "00".trim().trim_start_matches('0');
547        assert_eq!(code, "");
548
549        // "01" → "1" (JGD2000)
550        let code = "01".trim().trim_start_matches('0');
551        assert_eq!(code, "1");
552
553        // "02" → "2" (JGD2011)
554        let code = "02".trim().trim_start_matches('0');
555        assert_eq!(code, "2");
556    }
557
558    #[test]
559    fn test_tky2jgd_transform() {
560        // TKY2JGD.par が存在する場合のみテスト実行
561        // ワークスペースルートからのパスを構築
562        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
563        let tky_path = workspace_root.join("params/TKY2JGD.par");
564        if !tky_path.exists() {
565            println!("Skipping test: TKY2JGD.par not found at {:?}", tky_path);
566            return;
567        }
568
569        let transformer = JgdTransformer::new_tky_only(tky_path)
570            .expect("Failed to create transformer");
571
572        // 東京駅付近 (Tokyo Datum)
573        // 日本測地系: 35°40'00", 139°46'00"
574        let (lat, lon) = transformer
575            .transform_horizontal(35.666667, 139.766667, "00")
576            .expect("Transform failed");
577
578        // Tokyo→JGD2000では約10秒(約300m)のずれがある
579        // JGD2000座標は元より北西にずれる
580        assert!((lat - 35.666667).abs() < 0.01, "Latitude shift too large");
581        assert!((lon - 139.766667).abs() < 0.01, "Longitude shift too large");
582
583        // 変換後は元と異なる値になるはず
584        assert!((lat - 35.666667).abs() > 0.0001, "Latitude should have changed");
585        assert!((lon - 139.766667).abs() > 0.0001, "Longitude should have changed");
586
587        println!("Tokyo Datum: (35.666667, 139.766667)");
588        println!("JGD2000:     ({}, {})", lat, lon);
589    }
590
591    #[test]
592    fn test_jgd2011_no_transform() {
593        // JGD2011座標は変換不要
594        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
595        let tky_path = workspace_root.join("params/TKY2JGD.par");
596        if !tky_path.exists() {
597            return;
598        }
599
600        let transformer = JgdTransformer::new_tky_only(&tky_path)
601            .expect("Failed to create transformer");
602
603        let (lat, lon) = transformer
604            .transform_horizontal(35.666667, 139.766667, "02")
605            .expect("Transform failed");
606
607        // JGD2011は変換しないので同じ値
608        assert!((lat - 35.666667).abs() < 0.000001);
609        assert!((lon - 139.766667).abs() < 0.000001);
610    }
611
612    #[test]
613    fn test_full_transform_with_survey_date_before_earthquake() {
614        // 調査日が東北地震前 → 地震補正が適用される
615        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
616        let tky_path = workspace_root.join("params/TKY2JGD.par");
617        let patch_dir = workspace_root.join("params/patch_files");
618
619        if !tky_path.exists() || !patch_dir.exists() {
620            println!("Skipping test: params not found");
621            return;
622        }
623
624        // 2010年の調査(東北地震2011/3/11より前)
625        let survey_date = SurveyStartDate::new(2010, 5, 15);
626        let transformer = JgdTransformer::with_survey_date(&tky_path, &patch_dir, survey_date)
627            .expect("Failed to create transformer");
628
629        // 仙台駅付近 (JGD2000) - 標高15m
630        let result = transformer
631            .transform_full(38.2601, 140.8824, 15.0, "01")
632            .expect("Transform failed");
633
634        println!("Survey date: 2010/5/15 (before Tohoku earthquake)");
635        println!("JGD2000: (38.2601, 140.8824, 15.0m)");
636        println!("JGD2011: ({}, {}, {}m)", result.0, result.1, result.2);
637
638        // 東北地震補正が適用されるため、標高が変化するはず
639        // (ジオイド補正 + 地震補正)
640    }
641
642    #[test]
643    fn test_full_transform_with_survey_date_after_earthquake() {
644        // 調査日が東北地震後 → 地震補正は適用されない(ジオイド補正のみ)
645        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
646        let tky_path = workspace_root.join("params/TKY2JGD.par");
647        let patch_dir = workspace_root.join("params/patch_files");
648
649        if !tky_path.exists() || !patch_dir.exists() {
650            println!("Skipping test: params not found");
651            return;
652        }
653
654        // 2015年の調査(東北地震2011/3/11より後)
655        let survey_date = SurveyStartDate::new(2015, 6, 20);
656        let transformer = JgdTransformer::with_survey_date(&tky_path, &patch_dir, survey_date)
657            .expect("Failed to create transformer");
658
659        // 仙台駅付近 (JGD2000) - 標高15m
660        let result = transformer
661            .transform_full(38.2601, 140.8824, 15.0, "01")
662            .expect("Transform failed");
663
664        println!("Survey date: 2015/6/20 (after Tohoku earthquake)");
665        println!("JGD2000: (38.2601, 140.8824, 15.0m)");
666        println!("JGD2011: ({}, {}, {}m)", result.0, result.1, result.2);
667
668        // 地震補正は適用されない(ジオイド補正のみ)
669    }
670
671    #[test]
672    fn test_survey_date_parsing() {
673        // YYYYMMDD形式
674        let date = SurveyStartDate::from_yyyymmdd("20101015");
675        assert!(date.is_some());
676        let date = date.unwrap();
677        assert_eq!(date.year, 2010);
678        assert_eq!(date.month, 10);
679        assert_eq!(date.day, 15);
680
681        // 個別パース
682        let date = SurveyStartDate::from_parts(Some("2011"), Some("3"), Some("10"));
683        assert!(date.is_some());
684        let date = date.unwrap();
685        assert_eq!(date.year, 2011);
686        assert_eq!(date.month, 3);
687        assert_eq!(date.day, 10);
688    }
689
690    #[test]
691    fn test_earthquake_correction_should_apply() {
692        // 地震前の調査 → 適用する
693        let before = SurveyStartDate::new(2010, 1, 1);
694        assert!(should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(before)));
695
696        // 地震後の調査 → 適用しない
697        let after = SurveyStartDate::new(2012, 1, 1);
698        assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(after)));
699
700        // ジオイド補正は常に適用
701        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", Some(before)));
702        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", Some(after)));
703        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", None));
704
705        // 調査日不明の場合、地震補正は適用しない
706        assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", None));
707    }
708}