use crate::error::ProjError;
#[cfg(feature = "no_std")]
use alloc::format;
#[cfg(feature = "no_std")]
use alloc::string::String;
#[cfg(feature = "no_std")]
use alloc::string::ToString;
use core::fmt;
use core::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Epoch {
pub year: f64,
}
impl Epoch {
pub fn new(year: f64) -> Result<Self, ProjError> {
if !year.is_finite() {
return Err(ProjError::IllegalArgValue);
}
Ok(Self { year })
}
pub fn from_decimal_year(year: f64) -> Result<Self, ProjError> {
Self::new(year)
}
#[must_use]
pub fn delta_years(&self, other: &Epoch) -> f64 {
self.year - other.year
}
}
impl fmt::Display for Epoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.year)
}
}
impl FromStr for Epoch {
type Err = ProjError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let year = s
.trim()
.parse::<f64>()
.map_err(|_| ProjError::IllegalArgValue)?;
Self::new(year)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RefFrameEpoch {
pub frame: String,
pub epoch: Option<Epoch>,
}
impl RefFrameEpoch {
pub fn new(frame: impl Into<String>, epoch: Option<Epoch>) -> Self {
Self {
frame: frame.into(),
epoch,
}
}
pub fn static_frame(frame: impl Into<String>) -> Self {
Self::new(frame, None)
}
#[must_use]
pub fn is_dynamic(&self) -> bool {
self.epoch.is_some()
}
}
impl fmt::Display for RefFrameEpoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.epoch {
None => write!(f, "{}", self.frame),
Some(e) => write!(f, "{}@{}", self.frame, e),
}
}
}
impl FromStr for RefFrameEpoch {
type Err = ProjError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if let Some((frame, epoch_str)) = s.split_once('@') {
let epoch = Epoch::from_str(epoch_str)?;
Ok(Self::new(frame.trim(), Some(epoch)))
} else {
Ok(Self::static_frame(s))
}
}
}
#[derive(Debug, Clone)]
pub struct EpochCoord {
pub coord: crate::coord::Coord,
pub frame: RefFrameEpoch,
}
impl EpochCoord {
pub fn new(coord: crate::coord::Coord, frame: RefFrameEpoch) -> Self {
Self { coord, frame }
}
pub fn from_lonlat(lon: f64, lat: f64, frame: RefFrameEpoch) -> Self {
Self::new(
crate::coord::Coord::new(lon, lat, 0.0, f64::INFINITY),
frame,
)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EpochTaggedCrs {
pub crs_id: String,
pub epoch: Option<f64>,
}
impl EpochTaggedCrs {
pub fn parse(s: &str) -> Result<Self, EpochParseError> {
if let Some((before, after)) = s.split_once('@') {
let crs_id = before.trim().to_string();
if crs_id.is_empty() {
return Err(EpochParseError::EmptyCrsId);
}
let epoch_str = after.trim();
let epoch: f64 = epoch_str
.parse()
.map_err(|_| EpochParseError::InvalidEpoch(epoch_str.to_string()))?;
if !(1900.0..=2200.0).contains(&epoch) {
return Err(EpochParseError::EpochOutOfRange);
}
Ok(Self {
crs_id,
epoch: Some(epoch),
})
} else {
let crs_id = s.trim().to_string();
if crs_id.is_empty() {
return Err(EpochParseError::EmptyCrsId);
}
Ok(Self {
crs_id,
epoch: None,
})
}
}
#[must_use]
pub fn to_string_repr(&self) -> String {
match self.epoch {
Some(e) => format!("{}@{}", self.crs_id, e),
None => self.crs_id.clone(),
}
}
}
impl fmt::Display for EpochTaggedCrs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string_repr())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EpochParseError {
EmptyCrsId,
InvalidEpoch(String),
EpochOutOfRange,
}
impl fmt::Display for EpochParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EpochParseError::EmptyCrsId => write!(f, "CRS identifier is empty"),
EpochParseError::InvalidEpoch(s) => write!(f, "invalid epoch: {:?}", s),
EpochParseError::EpochOutOfRange => {
write!(f, "epoch out of range [1900, 2200]")
}
}
}
}
pub mod epsg_pmo {
pub const ITRF2014_PMO: u32 = 1066;
pub const ITRF2020_PMO: u32 = 1067;
pub const NNR_MORVEL56: u32 = 1085;
pub const NOAM_PLATE: u32 = 1086;
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "no_std")]
use alloc::string::ToString;
#[test]
fn parse_epoch() {
let e: Epoch = "2021.3".parse().unwrap();
assert!((e.year - 2021.3).abs() < 1e-10);
}
#[test]
fn parse_ref_frame_epoch_with_at() {
let r: RefFrameEpoch = "ITRF2020@2021.3".parse().unwrap();
assert_eq!(r.frame, "ITRF2020");
assert!(r.epoch.is_some());
assert!((r.epoch.unwrap().year - 2021.3).abs() < 1e-10);
assert!(r.is_dynamic());
}
#[test]
fn parse_ref_frame_epoch_static() {
let r: RefFrameEpoch = "NAD83(2011)".parse().unwrap();
assert_eq!(r.frame, "NAD83(2011)");
assert!(r.epoch.is_none());
assert!(!r.is_dynamic());
}
#[test]
fn display_round_trips() {
let r: RefFrameEpoch = "ITRF2020@2021.3".parse().unwrap();
assert_eq!(r.to_string(), "ITRF2020@2021.3");
let s = RefFrameEpoch::static_frame("WGS84");
assert_eq!(s.to_string(), "WGS84");
}
#[test]
fn delta_years() {
let e1 = Epoch::new(2020.0).unwrap();
let e2 = Epoch::new(2021.5).unwrap();
assert!((e2.delta_years(&e1) - 1.5).abs() < 1e-10);
}
#[test]
fn epoch_coord_construction() {
let frame: RefFrameEpoch = "ITRF2020@2021.3".parse().unwrap();
let ec = EpochCoord::from_lonlat(10.0, 51.0, frame);
assert!((ec.coord[0] - 10.0).abs() < 1e-12);
assert!((ec.coord[1] - 51.0).abs() < 1e-12);
}
#[test]
fn epoch_invalid_nan() {
assert!(Epoch::new(f64::NAN).is_err());
}
#[test]
fn epoch_invalid_infinite() {
assert!(Epoch::new(f64::INFINITY).is_err());
}
#[test]
fn epoch_from_decimal_year() {
let e = Epoch::from_decimal_year(2024.0).unwrap();
assert!((e.year - 2024.0).abs() < 1e-10);
}
#[test]
fn epoch_parse_error_on_garbage() {
let result = "not_a_number".parse::<Epoch>();
assert!(result.is_err());
}
#[test]
fn ref_frame_epoch_new_with_epoch() {
let e = Epoch::new(2020.0).unwrap();
let r = RefFrameEpoch::new("ITRF2014", Some(e));
assert_eq!(r.frame, "ITRF2014");
assert!(r.is_dynamic());
}
#[test]
fn epsg_pmo_constants() {
assert_eq!(epsg_pmo::ITRF2014_PMO, 1066);
assert_eq!(epsg_pmo::ITRF2020_PMO, 1067);
assert_eq!(epsg_pmo::NNR_MORVEL56, 1085);
assert_eq!(epsg_pmo::NOAM_PLATE, 1086);
}
#[test]
fn delta_years_negative() {
let earlier = Epoch::new(2022.0).unwrap();
let later = Epoch::new(2019.5).unwrap();
assert!((later.delta_years(&earlier) - (-2.5)).abs() < 1e-10);
}
#[test]
fn epoch_display() {
let e = Epoch::new(2021.5).unwrap();
assert_eq!(e.to_string(), "2021.5");
}
#[test]
fn epoch_coord_new() {
let frame = RefFrameEpoch::static_frame("WGS84");
let coord = crate::coord::Coord::new(1.0, 2.0, 3.0, 4.0);
let ec = EpochCoord::new(coord, frame.clone());
assert_eq!(ec.frame, frame);
assert!((ec.coord[2] - 3.0).abs() < 1e-12);
}
#[test]
fn epoch_negative_year_valid() {
let e = Epoch::new(-500.0).unwrap();
assert!((e.year - (-500.0)).abs() < 1e-10);
}
#[test]
fn from_str_whitespace_trimmed() {
let e: Epoch = " 2023.75 ".parse().unwrap();
assert!((e.year - 2023.75).abs() < 1e-10);
let r: RefFrameEpoch = " ITRF2020@2023.75 ".parse().unwrap();
assert_eq!(r.frame, "ITRF2020");
assert!((r.epoch.unwrap().year - 2023.75).abs() < 1e-10);
}
#[test]
fn epoch_tagged_crs_parse_basic() {
let e = EpochTaggedCrs::parse("ITRF2020@2021.3").unwrap();
assert_eq!(e.crs_id, "ITRF2020");
assert!((e.epoch.unwrap() - 2021.3).abs() < 1e-10);
}
#[test]
fn epoch_tagged_crs_parse_no_epoch() {
let e = EpochTaggedCrs::parse("ITRF2020").unwrap();
assert_eq!(e.crs_id, "ITRF2020");
assert!(e.epoch.is_none());
}
#[test]
fn epoch_tagged_crs_parse_epsg() {
let e = EpochTaggedCrs::parse("EPSG:4326@2020.0").unwrap();
assert_eq!(e.crs_id, "EPSG:4326");
assert_eq!(e.epoch, Some(2020.0));
}
#[test]
fn epoch_tagged_crs_roundtrip() {
let s = "ITRF2020@2021.3";
let e = EpochTaggedCrs::parse(s).unwrap();
assert_eq!(e.to_string_repr(), s);
}
#[test]
fn epoch_tagged_crs_out_of_range() {
assert!(EpochTaggedCrs::parse("X@1800.0").is_err());
}
#[test]
fn epoch_tagged_crs_empty_crs_id_error() {
assert!(EpochTaggedCrs::parse("").is_err());
assert!(EpochTaggedCrs::parse("@2021.3").is_err());
}
#[test]
fn epoch_tagged_crs_invalid_epoch_error() {
assert!(EpochTaggedCrs::parse("ITRF2020@not_a_number").is_err());
}
#[test]
fn epoch_tagged_crs_display() {
let e = EpochTaggedCrs::parse("ITRF2020@2021.3").unwrap();
assert_eq!(e.to_string(), "ITRF2020@2021.3");
}
}