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統合)
148pub struct JgdTransformer {
149    /// Tokyo Datum → JGD2000 変換器
150    tky_tf: Transformer<ParData>,
151    /// JGD2000 → JGD2011 統合変換器 (水平+垂直)
152    unified_tf: Transformer<ParData>,
153}
154
155impl JgdTransformer {
156    /// パラメータファイルを読み込んで変換器を初期化(調査日指定あり)
157    ///
158    /// # Arguments
159    /// * `tky_path` - TKY2JGD.par のパス
160    /// * `patch_dir` - PatchJGD系ファイルが入ったディレクトリのパス
161    /// * `survey_date` - 調査日(地震補正の適用判定に使用)
162    ///
163    /// # 地震補正の適用ルール
164    /// - 調査日が地震発生日より**前**の場合のみ、その地震の補正を適用
165    /// - 調査日が地震発生日より**後**の場合、補正は適用しない(既に変動後の位置)
166    /// - ジオイド補正(hyokorev2014_geoid2011_h.par)は常に適用
167    ///
168    /// # Example
169    /// ```ignore
170    /// let survey_date = SurveyStartDate::new(2010, 5, 15);
171    /// let transformer = JgdTransformer::with_survey_date(
172    ///     "params/TKY2JGD.par",
173    ///     "params/patch_files",
174    ///     survey_date
175    /// )?;
176    /// ```
177    pub fn with_survey_date<P: AsRef<Path>>(
178        tky_path: P,
179        patch_dir: P,
180        survey_date: SurveyStartDate,
181    ) -> Result<Self, TransformError> {
182        let tky_content = read_param_file(&tky_path)?;
183        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
184            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
185
186        let unified_tf = Self::create_unified_transformer(patch_dir, Some(survey_date))?;
187
188        Ok(Self { tky_tf, unified_tf })
189    }
190
191    /// パラメータファイルを読み込んで変換器を初期化(調査日なし)
192    ///
193    /// **注意**: 調査日が不明な場合、地震補正は適用されません。
194    /// ジオイド補正のみが適用されます。
195    ///
196    /// # Arguments
197    /// * `tky_path` - TKY2JGD.par のパス
198    /// * `patch_dir` - PatchJGD系ファイルが入ったディレクトリのパス
199    pub fn new<P: AsRef<Path>>(tky_path: P, patch_dir: P) -> Result<Self, TransformError> {
200        let tky_content = read_param_file(&tky_path)?;
201        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
202            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
203
204        // 調査日なしの場合、地震補正は適用しない(ジオイド補正のみ)
205        let unified_tf = Self::create_unified_transformer(patch_dir, None)?;
206
207        Ok(Self { tky_tf, unified_tf })
208    }
209
210    /// TKY2JGDのみで変換器を初期化 (PatchJGDファイルなし)
211    ///
212    /// JGD2000→JGD2011変換が不要な場合に使用
213    pub fn new_tky_only<P: AsRef<Path>>(tky_path: P) -> Result<Self, TransformError> {
214        let tky_content = read_param_file(&tky_path)?;
215        let tky_tf = Transformer::from_str(&tky_content, Format::TKY2JGD)
216            .map_err(|e| TransformError::Parse(format!("TKY2JGD parse error: {}", e)))?;
217
218        // 空の統合変換器を作成
219        let empty_params: HashMap<u32, Parameter> = HashMap::new();
220        let empty_data =
221            ParData::with_description(Format::PatchJGD_HV, empty_params, "Empty".to_string());
222        let unified_tf = Transformer::new(empty_data);
223
224        Ok(Self { tky_tf, unified_tf })
225    }
226
227    /// ディレクトリ内のパラメータファイルを加算マージして1つのTransformerを作る
228    ///
229    /// # Directory Structure
230    /// ```text
231    /// patch_dir/
232    /// ├── horizontal/    # 水平補正ファイル (PatchJGD形式)
233    /// │   ├── touhokutaiheiyouoki2011.par
234    /// │   └── ...
235    /// └── elevation/     # 標高補正ファイル (PatchJGD_H/HyokoRev形式)
236    ///     ├── touhokutaiheiyouoki2011_h.par
237    ///     ├── hyokorev2014_geoid2011_h.par
238    ///     └── ...
239    /// ```
240    ///
241    /// # Arguments
242    /// * `dir_path` - パラメータファイルのルートディレクトリ
243    /// * `survey_date` - 調査日(Noneの場合、地震補正は適用しない)
244    fn create_unified_transformer<P: AsRef<Path>>(
245        dir_path: P,
246        survey_date: Option<SurveyStartDate>,
247    ) -> Result<Transformer<ParData>, TransformError> {
248        let dir = dir_path.as_ref();
249
250        // ディレクトリが存在しない場合は空のTransformerを返す
251        if !dir.exists() {
252            let empty_params: HashMap<u32, Parameter> = HashMap::new();
253            let empty_data = ParData::with_description(
254                Format::PatchJGD_HV,
255                empty_params,
256                "No patch files".to_string(),
257            );
258            return Ok(Transformer::new(empty_data));
259        }
260
261        let mut unified_params: HashMap<u32, Parameter> = HashMap::new();
262        let mut description = String::from("Unified Parameter Set:\n");
263        if let Some(date) = survey_date {
264            description.push_str(&format!(
265                "Survey date: {}/{}/{}\n",
266                date.year, date.month, date.day
267            ));
268        } else {
269            description.push_str("Survey date: unknown (earthquake corrections disabled)\n");
270        }
271        let mut loaded_count = 0;
272
273        // horizontal/ と elevation/ サブディレクトリをスキャン
274        let horizontal_dir = dir.join("horizontal");
275        let elevation_dir = dir.join("elevation");
276
277        // 水平補正ファイルを読み込み
278        if horizontal_dir.exists() {
279            description.push_str("\n[Horizontal corrections]\n");
280            loaded_count += Self::load_param_files(
281                &horizontal_dir,
282                Format::PatchJGD,
283                survey_date,
284                &mut unified_params,
285                &mut description,
286            )?;
287        }
288
289        // 標高補正ファイルを読み込み
290        if elevation_dir.exists() {
291            description.push_str("\n[Elevation corrections]\n");
292            loaded_count += Self::load_param_files_elevation(
293                &elevation_dir,
294                survey_date,
295                &mut unified_params,
296                &mut description,
297            )?;
298        }
299
300        if loaded_count == 0 {
301            description = "No patch files loaded".to_string();
302        }
303
304        let unified_data =
305            ParData::with_description(Format::PatchJGD_HV, unified_params, description);
306        Ok(Transformer::new(unified_data))
307    }
308
309    /// 指定ディレクトリからパラメータファイルを読み込む(水平用)
310    fn load_param_files(
311        dir: &Path,
312        format: Format,
313        survey_date: Option<SurveyStartDate>,
314        unified_params: &mut HashMap<u32, Parameter>,
315        description: &mut String,
316    ) -> Result<usize, TransformError> {
317        let mut loaded_count = 0;
318
319        for entry in fs::read_dir(dir)? {
320            let entry = entry?;
321            let path = entry.path();
322
323            if path.extension().and_then(|s| s.to_str()) != Some("par") {
324                continue;
325            }
326
327            let file_name = match path.file_name().and_then(|s| s.to_str()) {
328                Some(name) => name.to_string(),
329                None => continue,
330            };
331
332            // 地震補正の適用判定
333            if !should_apply_earthquake_correction(&file_name, survey_date) {
334                description.push_str(&format!("  - {} (skipped: survey after earthquake)\n", file_name));
335                continue;
336            }
337
338            let content = read_param_file(&path)?;
339            let par_data = ParData::from_str(&content, format)
340                .map_err(|e| TransformError::Parse(format!("{}: {}", file_name, e)))?;
341
342            for (mesh_code, param) in par_data.parameter {
343                unified_params
344                    .entry(mesh_code)
345                    .and_modify(|existing| {
346                        existing.latitude += param.latitude;
347                        existing.longitude += param.longitude;
348                        existing.altitude += param.altitude;
349                    })
350                    .or_insert(param);
351            }
352
353            description.push_str(&format!("  - {} (applied)\n", file_name));
354            loaded_count += 1;
355        }
356
357        Ok(loaded_count)
358    }
359
360    /// 指定ディレクトリからパラメータファイルを読み込む(標高用)
361    fn load_param_files_elevation(
362        dir: &Path,
363        survey_date: Option<SurveyStartDate>,
364        unified_params: &mut HashMap<u32, Parameter>,
365        description: &mut String,
366    ) -> Result<usize, TransformError> {
367        let mut loaded_count = 0;
368
369        for entry in fs::read_dir(dir)? {
370            let entry = entry?;
371            let path = entry.path();
372
373            if path.extension().and_then(|s| s.to_str()) != Some("par") {
374                continue;
375            }
376
377            let file_name = match path.file_name().and_then(|s| s.to_str()) {
378                Some(name) => name.to_string(),
379                None => continue,
380            };
381
382            // 地震補正の適用判定
383            if !should_apply_earthquake_correction(&file_name, survey_date) {
384                description.push_str(&format!("  - {} (skipped: survey after earthquake)\n", file_name));
385                continue;
386            }
387
388            // ファイル名でフォーマットを判別
389            let format = if file_name.contains("hyoko") {
390                Format::HyokoRev // ジオイドモデル改定
391            } else {
392                Format::PatchJGD_H // 地震(標高)
393            };
394
395            let content = read_param_file(&path)?;
396            let par_data = ParData::from_str(&content, format)
397                .map_err(|e| TransformError::Parse(format!("{}: {}", file_name, e)))?;
398
399            for (mesh_code, param) in par_data.parameter {
400                unified_params
401                    .entry(mesh_code)
402                    .and_modify(|existing| {
403                        existing.latitude += param.latitude;
404                        existing.longitude += param.longitude;
405                        existing.altitude += param.altitude;
406                    })
407                    .or_insert(param);
408            }
409
410            description.push_str(&format!("  - {} (applied)\n", file_name));
411            loaded_count += 1;
412        }
413
414        Ok(loaded_count)
415    }
416
417    /// 座標をJGD2011に変換 (水平座標のみ)
418    ///
419    /// # Arguments
420    /// * `lat` - 緯度 (度, 10進数)
421    /// * `lon` - 経度 (度, 10進数)
422    /// * `system_code` - 測地系コード ("00"=Tokyo, "01"=JGD2000, "02"=JGD2011)
423    ///
424    /// # Returns
425    /// 変換後の (緯度, 経度) または変換失敗時にNone
426    pub fn transform_horizontal(
427        &self,
428        lat: f64,
429        lon: f64,
430        system_code: &str,
431    ) -> Option<(f64, f64)> {
432        let result = self.transform_point(lat, lon, 0.0, system_code).ok()?;
433        Some((result.latitude, result.longitude))
434    }
435
436    /// 座標をJGD2011に変換 (水平+垂直)
437    ///
438    /// # Arguments
439    /// * `lat` - 緯度 (度, 10進数)
440    /// * `lon` - 経度 (度, 10進数)
441    /// * `alt` - 標高 (m)
442    /// * `system_code` - 測地系コード ("00"=Tokyo, "01"=JGD2000, "02"=JGD2011)
443    ///
444    /// # Returns
445    /// 変換後の (緯度, 経度, 標高)
446    pub fn transform_full(
447        &self,
448        lat: f64,
449        lon: f64,
450        alt: f64,
451        system_code: &str,
452    ) -> Result<(f64, f64, f64), TransformError> {
453        let result = self.transform_point(lat, lon, alt, system_code)?;
454        Ok((result.latitude, result.longitude, result.altitude))
455    }
456
457    /// 内部変換処理
458    fn transform_point(
459        &self,
460        lat: f64,
461        lon: f64,
462        alt: f64,
463        system_code: &str,
464    ) -> Result<Point, TransformError> {
465        let input_point = Point::new_unchecked(lat, lon, alt);
466
467        // コードの正規化 (先頭のゼロ除去)
468        let code = system_code.trim().trim_start_matches('0');
469
470        match code {
471            // Tokyo Datum (日本測地系) → Step 1 + Step 2
472            // code "00" の場合、trim_start_matches('0') で "" になる
473            "" | "0" => {
474                // Step 1: Tokyo → JGD2000
475                let corr_tky = self
476                    .tky_tf
477                    .forward_corr(&input_point)
478                    .map_err(|e| TransformError::Transform(format!("TKY2JGD: {}", e)))?;
479                let p_2000 = &input_point + corr_tky;
480
481                // Step 2: JGD2000 → JGD2011
482                match self.unified_tf.forward_corr(&p_2000) {
483                    Ok(corr_unified) => Ok(&p_2000 + corr_unified),
484                    Err(_) => Ok(p_2000), // パッチ範囲外の場合はJGD2000のまま
485                }
486            }
487
488            // JGD2000 (世界測地系2000) → Step 2のみ
489            "1" => match self.unified_tf.forward_corr(&input_point) {
490                Ok(corr_unified) => Ok(&input_point + corr_unified),
491                Err(_) => Ok(input_point), // パッチ範囲外の場合はそのまま
492            },
493
494            // JGD2011 → 変換不要
495            "2" => Ok(input_point),
496
497            // 未知のコード → そのまま返す
498            _ => Ok(input_point),
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn test_code_normalization() {
509        // "00" → "" (Tokyo Datum)
510        let code = "00".trim().trim_start_matches('0');
511        assert_eq!(code, "");
512
513        // "01" → "1" (JGD2000)
514        let code = "01".trim().trim_start_matches('0');
515        assert_eq!(code, "1");
516
517        // "02" → "2" (JGD2011)
518        let code = "02".trim().trim_start_matches('0');
519        assert_eq!(code, "2");
520    }
521
522    #[test]
523    fn test_tky2jgd_transform() {
524        // TKY2JGD.par が存在する場合のみテスト実行
525        // ワークスペースルートからのパスを構築
526        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
527        let tky_path = workspace_root.join("params/TKY2JGD.par");
528        if !tky_path.exists() {
529            println!("Skipping test: TKY2JGD.par not found at {:?}", tky_path);
530            return;
531        }
532
533        let transformer = JgdTransformer::new_tky_only(tky_path)
534            .expect("Failed to create transformer");
535
536        // 東京駅付近 (Tokyo Datum)
537        // 日本測地系: 35°40'00", 139°46'00"
538        let (lat, lon) = transformer
539            .transform_horizontal(35.666667, 139.766667, "00")
540            .expect("Transform failed");
541
542        // Tokyo→JGD2000では約10秒(約300m)のずれがある
543        // JGD2000座標は元より北西にずれる
544        assert!((lat - 35.666667).abs() < 0.01, "Latitude shift too large");
545        assert!((lon - 139.766667).abs() < 0.01, "Longitude shift too large");
546
547        // 変換後は元と異なる値になるはず
548        assert!((lat - 35.666667).abs() > 0.0001, "Latitude should have changed");
549        assert!((lon - 139.766667).abs() > 0.0001, "Longitude should have changed");
550
551        println!("Tokyo Datum: (35.666667, 139.766667)");
552        println!("JGD2000:     ({}, {})", lat, lon);
553    }
554
555    #[test]
556    fn test_jgd2011_no_transform() {
557        // JGD2011座標は変換不要
558        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
559        let tky_path = workspace_root.join("params/TKY2JGD.par");
560        if !tky_path.exists() {
561            return;
562        }
563
564        let transformer = JgdTransformer::new_tky_only(&tky_path)
565            .expect("Failed to create transformer");
566
567        let (lat, lon) = transformer
568            .transform_horizontal(35.666667, 139.766667, "02")
569            .expect("Transform failed");
570
571        // JGD2011は変換しないので同じ値
572        assert!((lat - 35.666667).abs() < 0.000001);
573        assert!((lon - 139.766667).abs() < 0.000001);
574    }
575
576    #[test]
577    fn test_full_transform_with_survey_date_before_earthquake() {
578        // 調査日が東北地震前 → 地震補正が適用される
579        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
580        let tky_path = workspace_root.join("params/TKY2JGD.par");
581        let patch_dir = workspace_root.join("params/patch_files");
582
583        if !tky_path.exists() || !patch_dir.exists() {
584            println!("Skipping test: params not found");
585            return;
586        }
587
588        // 2010年の調査(東北地震2011/3/11より前)
589        let survey_date = SurveyStartDate::new(2010, 5, 15);
590        let transformer = JgdTransformer::with_survey_date(&tky_path, &patch_dir, survey_date)
591            .expect("Failed to create transformer");
592
593        // 仙台駅付近 (JGD2000) - 標高15m
594        let result = transformer
595            .transform_full(38.2601, 140.8824, 15.0, "01")
596            .expect("Transform failed");
597
598        println!("Survey date: 2010/5/15 (before Tohoku earthquake)");
599        println!("JGD2000: (38.2601, 140.8824, 15.0m)");
600        println!("JGD2011: ({}, {}, {}m)", result.0, result.1, result.2);
601
602        // 東北地震補正が適用されるため、標高が変化するはず
603        // (ジオイド補正 + 地震補正)
604    }
605
606    #[test]
607    fn test_full_transform_with_survey_date_after_earthquake() {
608        // 調査日が東北地震後 → 地震補正は適用されない(ジオイド補正のみ)
609        let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
610        let tky_path = workspace_root.join("params/TKY2JGD.par");
611        let patch_dir = workspace_root.join("params/patch_files");
612
613        if !tky_path.exists() || !patch_dir.exists() {
614            println!("Skipping test: params not found");
615            return;
616        }
617
618        // 2015年の調査(東北地震2011/3/11より後)
619        let survey_date = SurveyStartDate::new(2015, 6, 20);
620        let transformer = JgdTransformer::with_survey_date(&tky_path, &patch_dir, survey_date)
621            .expect("Failed to create transformer");
622
623        // 仙台駅付近 (JGD2000) - 標高15m
624        let result = transformer
625            .transform_full(38.2601, 140.8824, 15.0, "01")
626            .expect("Transform failed");
627
628        println!("Survey date: 2015/6/20 (after Tohoku earthquake)");
629        println!("JGD2000: (38.2601, 140.8824, 15.0m)");
630        println!("JGD2011: ({}, {}, {}m)", result.0, result.1, result.2);
631
632        // 地震補正は適用されない(ジオイド補正のみ)
633    }
634
635    #[test]
636    fn test_survey_date_parsing() {
637        // YYYYMMDD形式
638        let date = SurveyStartDate::from_yyyymmdd("20101015");
639        assert!(date.is_some());
640        let date = date.unwrap();
641        assert_eq!(date.year, 2010);
642        assert_eq!(date.month, 10);
643        assert_eq!(date.day, 15);
644
645        // 個別パース
646        let date = SurveyStartDate::from_parts(Some("2011"), Some("3"), Some("10"));
647        assert!(date.is_some());
648        let date = date.unwrap();
649        assert_eq!(date.year, 2011);
650        assert_eq!(date.month, 3);
651        assert_eq!(date.day, 10);
652    }
653
654    #[test]
655    fn test_earthquake_correction_should_apply() {
656        // 地震前の調査 → 適用する
657        let before = SurveyStartDate::new(2010, 1, 1);
658        assert!(should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(before)));
659
660        // 地震後の調査 → 適用しない
661        let after = SurveyStartDate::new(2012, 1, 1);
662        assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(after)));
663
664        // ジオイド補正は常に適用
665        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", Some(before)));
666        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", Some(after)));
667        assert!(should_apply_earthquake_correction("hyokorev2014_geoid2011_h.par", None));
668
669        // 調査日不明の場合、地震補正は適用しない
670        assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", None));
671    }
672}