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 {
149 tky_tf: Transformer<ParData>,
151 unified_tf: Transformer<ParData>,
153}
154
155impl JgdTransformer {
156 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 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 let unified_tf = Self::create_unified_transformer(patch_dir, None)?;
206
207 Ok(Self { tky_tf, unified_tf })
208 }
209
210 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 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 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 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 let horizontal_dir = dir.join("horizontal");
275 let elevation_dir = dir.join("elevation");
276
277 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 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 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 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 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 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 let format = if file_name.contains("hyoko") {
390 Format::HyokoRev } else {
392 Format::PatchJGD_H };
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 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 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 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 let code = system_code.trim().trim_start_matches('0');
469
470 match code {
471 "" | "0" => {
474 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 match self.unified_tf.forward_corr(&p_2000) {
483 Ok(corr_unified) => Ok(&p_2000 + corr_unified),
484 Err(_) => Ok(p_2000), }
486 }
487
488 "1" => match self.unified_tf.forward_corr(&input_point) {
490 Ok(corr_unified) => Ok(&input_point + corr_unified),
491 Err(_) => Ok(input_point), },
493
494 "2" => Ok(input_point),
496
497 _ => Ok(input_point),
499 }
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn test_code_normalization() {
509 let code = "00".trim().trim_start_matches('0');
511 assert_eq!(code, "");
512
513 let code = "01".trim().trim_start_matches('0');
515 assert_eq!(code, "1");
516
517 let code = "02".trim().trim_start_matches('0');
519 assert_eq!(code, "2");
520 }
521
522 #[test]
523 fn test_tky2jgd_transform() {
524 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 let (lat, lon) = transformer
539 .transform_horizontal(35.666667, 139.766667, "00")
540 .expect("Transform failed");
541
542 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 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 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 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 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 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 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 }
605
606 #[test]
607 fn test_full_transform_with_survey_date_after_earthquake() {
608 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 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 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 }
634
635 #[test]
636 fn test_survey_date_parsing() {
637 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 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 let before = SurveyStartDate::new(2010, 1, 1);
658 assert!(should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(before)));
659
660 let after = SurveyStartDate::new(2012, 1, 1);
662 assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", Some(after)));
663
664 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 assert!(!should_apply_earthquake_correction("touhokutaiheiyouoki2011.par", None));
671 }
672}