1use 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#[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#[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 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 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
70struct EarthquakeInfo {
72 file_pattern: &'static str,
74 date: SurveyStartDate,
76}
77
78const 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
111fn should_apply_earthquake_correction(file_name: &str, survey_date: Option<SurveyStartDate>) -> bool {
113 let earthquake = EARTHQUAKES.iter().find(|eq| file_name.contains(eq.file_pattern));
115
116 match (earthquake, survey_date) {
117 (Some(eq), Some(date)) => {
119 date < eq.date
121 }
122 (Some(_), None) => false,
124 (None, _) => true,
126 }
127}
128
129pub fn read_param_file<P: AsRef<Path>>(path: P) -> Result<String, std::io::Error> {
131 let bytes = fs::read(&path)?;
132
133 if let Ok(s) = String::from_utf8(bytes.clone()) {
135 return Ok(s);
136 }
137
138 let (decoded, _, _) = SHIFT_JIS.decode(&bytes);
140 Ok(decoded.into_owned())
141}
142
143pub struct JgdTransformer {
150 tky_tf: Transformer<ParData>,
152 unified_tf: Transformer<ParData>,
154 jgd2011_to_wgs84: proj::Proj,
156}
157
158impl JgdTransformer {
159 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 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 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 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 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 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 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 let horizontal_dir = dir.join("horizontal");
287 let elevation_dir = dir.join("elevation");
288
289 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 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 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 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 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 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 let format = if file_name.contains("hyoko") {
402 Format::HyokoRev } else {
404 Format::PatchJGD_H };
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 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 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 pub fn to_wgs84(
481 &self,
482 lat: f64,
483 lon: f64,
484 system_code: &str,
485 ) -> Option<(f64, f64)> {
486 let (lat_jgd2011, lng_jgd2011) = self.transform_horizontal(lat, lon, system_code)?;
488
489 self.jgd2011_to_wgs84.convert((lng_jgd2011, lat_jgd2011)).ok()
491 }
492
493 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 let code = system_code.trim().trim_start_matches('0');
505
506 match code {
507 "" | "0" => {
510 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 match self.unified_tf.forward_corr(&p_2000) {
519 Ok(corr_unified) => Ok(&p_2000 + corr_unified),
520 Err(_) => Ok(p_2000), }
522 }
523
524 "1" => match self.unified_tf.forward_corr(&input_point) {
526 Ok(corr_unified) => Ok(&input_point + corr_unified),
527 Err(_) => Ok(input_point), },
529
530 "2" => Ok(input_point),
532
533 _ => Ok(input_point),
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_code_normalization() {
545 let code = "00".trim().trim_start_matches('0');
547 assert_eq!(code, "");
548
549 let code = "01".trim().trim_start_matches('0');
551 assert_eq!(code, "1");
552
553 let code = "02".trim().trim_start_matches('0');
555 assert_eq!(code, "2");
556 }
557
558 #[test]
559 fn test_tky2jgd_transform() {
560 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 let (lat, lon) = transformer
575 .transform_horizontal(35.666667, 139.766667, "00")
576 .expect("Transform failed");
577
578 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 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 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 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 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 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 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 }
641
642 #[test]
643 fn test_full_transform_with_survey_date_after_earthquake() {
644 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 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 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 }
670
671 #[test]
672 fn test_survey_date_parsing() {
673 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 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 let before = SurveyStartDate::new(2010, 1, 1);
694 assert!(should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(before)));
695
696 let after = SurveyStartDate::new(2012, 1, 1);
698 assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(after)));
699
700 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 assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", None));
707 }
708}