use crate::epoch_transform::{plate_for_frame, propagate_epoch};
use crate::frame_chain::{
apply_frame_transform, apply_frame_transform_inverse, find_frame_transform,
};
use oxiproj_core::{
epoch::Epoch,
plate_motion::{Plate, PlateMotionModel},
ProjError, ProjResult,
};
#[derive(Debug, Clone)]
pub struct EpochAwareResult {
pub x: f64,
pub y: f64,
pub z: f64,
pub from_frame: String,
pub from_epoch: Epoch,
pub to_frame: String,
pub to_epoch: Epoch,
}
fn resolve_plate(frame_name: &str) -> Option<Plate> {
if let Some(p) = plate_for_frame(frame_name) {
return Some(p);
}
let lower = frame_name.to_ascii_lowercase();
if lower.starts_with("itrf") || lower.starts_with("itrs") {
return Some(Plate::Eurasian);
}
None
}
#[allow(clippy::too_many_arguments)]
pub fn transform_epoch_aware(
x: f64,
y: f64,
z: f64,
from_frame: &str,
from_epoch: Epoch,
to_frame: &str,
to_epoch: Epoch,
model: PlateMotionModel,
) -> ProjResult<EpochAwareResult> {
let plate = resolve_plate(from_frame).ok_or(ProjError::NoOperation)?;
let (final_x, final_y, final_z) = if from_frame == to_frame {
propagate_epoch(x, y, z, &from_epoch, &to_epoch, model, plate)?
} else {
let (ft, is_inverse) = if let Some(ft) = find_frame_transform(from_frame, to_frame) {
(ft, false)
} else if let Some(ft) = find_frame_transform(to_frame, from_frame) {
(ft, true)
} else {
return Err(ProjError::NoOperation);
};
let ref_epoch_f64 = ft.ref_epoch;
let ref_epoch_val = Epoch::new(ref_epoch_f64)?;
let (x1, y1, z1) = propagate_epoch(x, y, z, &from_epoch, &ref_epoch_val, model, plate)?;
let (x2, y2, z2) = if is_inverse {
apply_frame_transform_inverse(ft, x1, y1, z1, ref_epoch_f64)?
} else {
apply_frame_transform(ft, x1, y1, z1, ref_epoch_f64)?
};
let plate2 = resolve_plate(to_frame).unwrap_or(plate);
propagate_epoch(x2, y2, z2, &ref_epoch_val, &to_epoch, model, plate2)?
};
Ok(EpochAwareResult {
x: final_x,
y: final_y,
z: final_z,
from_frame: from_frame.to_string(),
from_epoch,
to_frame: to_frame.to_string(),
to_epoch,
})
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::epoch::Epoch;
#[test]
fn test_itrf2014_to_itrf2020_epoch_shift() {
let (x0, y0, z0) = (3_711_000.0, 1_023_000.0, 5_036_000.0);
let result = transform_epoch_aware(
x0,
y0,
z0,
"ITRF2014",
Epoch::new(2010.0).unwrap(),
"ITRF2020",
Epoch::new(2015.0).unwrap(),
PlateMotionModel::Itrf2020,
)
.expect("should succeed");
assert!(result.x.is_finite() && result.y.is_finite() && result.z.is_finite());
let dx = result.x - x0;
let dy = result.y - y0;
let dz = result.z - z0;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
assert!(dist < 5.0, "displacement too large: {dist}m");
}
#[test]
fn test_same_frame_epoch_propagation() {
let result = transform_epoch_aware(
3_711_000.0,
1_023_000.0,
5_036_000.0,
"ITRF2020",
Epoch::new(2010.0).unwrap(),
"ITRF2020",
Epoch::new(2020.0).unwrap(),
PlateMotionModel::Itrf2020,
)
.expect("should succeed");
assert!(result.x.is_finite());
}
#[test]
fn test_unknown_frame_returns_error() {
let result = transform_epoch_aware(
1_000_000.0,
2_000_000.0,
3_000_000.0,
"UNKNOWNFRAME",
Epoch::new(2010.0).unwrap(),
"ITRF2020",
Epoch::new(2015.0).unwrap(),
PlateMotionModel::Itrf2020,
);
assert!(result.is_err(), "should fail for unknown frame");
}
#[test]
fn test_result_metadata_preserved() {
let result = transform_epoch_aware(
4_627_798.0,
119_795.0,
4_369_668.0,
"ITRF2020",
Epoch::new(2015.0).unwrap(),
"ITRF2014",
Epoch::new(2020.0).unwrap(),
PlateMotionModel::Itrf2014,
)
.expect("should succeed");
assert_eq!(result.from_frame, "ITRF2020");
assert_eq!(result.to_frame, "ITRF2014");
assert!((result.from_epoch.year - 2015.0).abs() < 1e-10);
assert!((result.to_epoch.year - 2020.0).abs() < 1e-10);
}
}