#[cfg(feature = "std")]
pub mod azimuthal;
#[cfg(feature = "std")]
pub mod conic;
#[cfg(feature = "std")]
pub mod cylindrical;
#[cfg(feature = "std")]
pub mod pseudocylindrical;
#[cfg(feature = "std")]
pub mod simd;
#[cfg(feature = "std")]
use crate::area_of_use::area_of_use_for_epsg;
#[cfg(feature = "std")]
use crate::crs::{Crs, CrsSource};
use crate::error::{Error, Result};
#[cfg(not(feature = "std"))]
use alloc::format;
use core::fmt;
#[cfg(feature = "std")]
use std::sync::Mutex;
#[cfg(feature = "std")]
use crate::proj_string::ProjString;
#[cfg(feature = "std")]
pub use azimuthal::{AzimuthalEquidistant, Gnomonic, LambertAzimuthalEqualArea};
#[cfg(feature = "std")]
pub use conic::{EquidistantConic, LambertConformalConic};
#[cfg(feature = "std")]
pub use cylindrical::{CassineSoldner, GaussKruger, TransverseMercator};
#[cfg(feature = "std")]
pub use pseudocylindrical::{EckertIV, EckertVI, Mollweide, Robinson, Sinusoidal};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coordinate {
pub x: f64,
pub y: f64,
}
impl Coordinate {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn from_lon_lat(lon: f64, lat: f64) -> Self {
Self::new(lon, lat)
}
pub fn lon(&self) -> f64 {
self.x
}
pub fn lat(&self) -> f64 {
self.y
}
pub fn validate_geographic(&self) -> Result<()> {
if !(-180.0..=180.0).contains(&self.x) {
return Err(Error::coordinate_out_of_bounds(self.x, self.y));
}
if !(-90.0..=90.0).contains(&self.y) {
return Err(Error::coordinate_out_of_bounds(self.x, self.y));
}
Ok(())
}
pub fn is_valid(&self) -> bool {
self.x.is_finite() && self.y.is_finite()
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coordinate3D {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Coordinate3D {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
pub fn to_2d(&self) -> Coordinate {
Coordinate::new(self.x, self.y)
}
pub fn is_valid(&self) -> bool {
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
}
}
impl From<Coordinate> for Coordinate3D {
fn from(coord: Coordinate) -> Self {
Self::new(coord.x, coord.y, 0.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
impl BoundingBox {
pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Result<Self> {
if min_x > max_x {
return Err(Error::invalid_bounding_box(format!(
"min_x ({}) > max_x ({})",
min_x, max_x
)));
}
if min_y > max_y {
return Err(Error::invalid_bounding_box(format!(
"min_y ({}) > max_y ({})",
min_y, max_y
)));
}
Ok(Self {
min_x,
min_y,
max_x,
max_y,
})
}
pub fn from_coordinates(c1: Coordinate, c2: Coordinate) -> Result<Self> {
let min_x = c1.x.min(c2.x);
let min_y = c1.y.min(c2.y);
let max_x = c1.x.max(c2.x);
let max_y = c1.y.max(c2.y);
Self::new(min_x, min_y, max_x, max_y)
}
pub fn width(&self) -> f64 {
self.max_x - self.min_x
}
pub fn height(&self) -> f64 {
self.max_y - self.min_y
}
pub fn center(&self) -> Coordinate {
Coordinate::new(
(self.min_x + self.max_x) / 2.0,
(self.min_y + self.max_y) / 2.0,
)
}
pub fn corners(&self) -> [Coordinate; 4] {
[
Coordinate::new(self.min_x, self.min_y),
Coordinate::new(self.max_x, self.min_y),
Coordinate::new(self.max_x, self.max_y),
Coordinate::new(self.min_x, self.max_y),
]
}
pub fn contains(&self, coord: &Coordinate) -> bool {
coord.x >= self.min_x
&& coord.x <= self.max_x
&& coord.y >= self.min_y
&& coord.y <= self.max_y
}
pub fn expand_to_include(&mut self, coord: &Coordinate) {
self.min_x = self.min_x.min(coord.x);
self.min_y = self.min_y.min(coord.y);
self.max_x = self.max_x.max(coord.x);
self.max_y = self.max_y.max(coord.y);
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AreaOfUseCheck {
#[default]
Off,
Warn,
Strict,
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AreaOfUseWarning {
pub lon: f64,
pub lat: f64,
pub epsg: u32,
pub west: f64,
pub south: f64,
pub east: f64,
pub north: f64,
}
#[cfg(feature = "std")]
pub struct Transformer {
source_crs: Crs,
target_crs: Crs,
proj: Option<proj4rs::Proj>,
strict: bool,
area_of_use_check: AreaOfUseCheck,
last_warning: Mutex<Option<AreaOfUseWarning>>,
source_epoch: Option<f64>,
target_epoch: Option<f64>,
itrf_params: Option<(crate::datum_transform::ItrfTransformParams, f64)>,
geoid: Option<std::sync::Arc<crate::geoid::GeoidGrid>>,
}
#[cfg(feature = "std")]
impl Transformer {
pub fn new(source_crs: Crs, target_crs: Crs) -> Result<Self> {
let is_compound = matches!(source_crs.source(), CrsSource::Compound { .. })
|| matches!(target_crs.source(), CrsSource::Compound { .. });
let either_engineering = source_crs.is_engineering() || target_crs.is_engineering();
let proj = if is_compound || either_engineering || source_crs.is_equivalent(&target_crs) {
None
} else {
let source_proj_str = source_crs.to_proj_string()?;
let target_proj_str = target_crs.to_proj_string()?;
let _source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
.map_err(|e| Error::projection_init_error(format!("Source CRS: {:?}", e)))?;
let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
.map_err(|e| Error::projection_init_error(format!("Target CRS: {:?}", e)))?;
Some(target_proj)
};
Ok(Self {
source_crs,
target_crs,
proj,
strict: true,
area_of_use_check: AreaOfUseCheck::default(),
last_warning: Mutex::new(None),
source_epoch: None,
target_epoch: None,
itrf_params: None,
geoid: None,
})
}
pub fn with_geoid(mut self, grid: std::sync::Arc<crate::geoid::GeoidGrid>) -> Self {
self.geoid = Some(grid);
self
}
pub fn geoid(&self) -> Option<&std::sync::Arc<crate::geoid::GeoidGrid>> {
self.geoid.as_ref()
}
pub fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn is_strict(&self) -> bool {
self.strict
}
pub fn with_area_of_use_check(mut self, mode: AreaOfUseCheck) -> Self {
self.area_of_use_check = mode;
if let Ok(mut slot) = self.last_warning.lock() {
*slot = None;
}
self
}
pub fn area_of_use_check(&self) -> AreaOfUseCheck {
self.area_of_use_check
}
pub fn last_warning(&self) -> Option<AreaOfUseWarning> {
self.last_warning.lock().ok().and_then(|guard| *guard)
}
pub fn clear_warning(&self) {
if let Ok(mut slot) = self.last_warning.lock() {
*slot = None;
}
}
fn check_area_of_use(&self, lon: f64, lat: f64) -> Result<()> {
if self.area_of_use_check == AreaOfUseCheck::Off {
return Ok(());
}
let epsg = match self.source_crs.epsg_code() {
Some(c) => c,
None => return Ok(()),
};
let aou = match area_of_use_for_epsg(epsg) {
Some(a) => a,
None => return Ok(()),
};
if aou.contains(lon, lat) {
return Ok(());
}
match self.area_of_use_check {
AreaOfUseCheck::Off => Ok(()),
AreaOfUseCheck::Warn => {
if let Ok(mut slot) = self.last_warning.lock() {
*slot = Some(AreaOfUseWarning {
lon,
lat,
epsg,
west: aou.west,
south: aou.south,
east: aou.east,
north: aou.north,
});
}
Ok(())
}
AreaOfUseCheck::Strict => Err(Error::OutsideAreaOfUse {
lon,
lat,
epsg,
west: aou.west,
south: aou.south,
east: aou.east,
north: aou.north,
}),
}
}
pub fn with_epoch(mut self, source_epoch: f64, target_epoch: f64) -> Result<Self> {
let src_itrf = self.source_crs.itrf_name().ok_or_else(|| {
Error::transformation_error(
"with_epoch requires the source CRS to be an ITRF realisation",
)
})?;
let dst_itrf = self.target_crs.itrf_name().ok_or_else(|| {
Error::transformation_error(
"with_epoch requires the target CRS to be an ITRF realisation",
)
})?;
let params_ref = crate::datum_transform::find_itrf_params(&src_itrf, &dst_itrf)
.ok_or_else(|| {
Error::transformation_error(format!(
"no ITRF parameters registered for {src_itrf} \u{2192} {dst_itrf}"
))
})?;
self.source_epoch = Some(source_epoch);
self.target_epoch = Some(target_epoch);
self.itrf_params = Some(params_ref);
Ok(self)
}
pub fn source_epoch(&self) -> Option<f64> {
self.source_epoch
}
pub fn target_epoch(&self) -> Option<f64> {
self.target_epoch
}
pub fn from_epsg(source_epsg: u32, target_epsg: u32) -> Result<Self> {
let source_crs = Crs::from_epsg(source_epsg)?;
let target_crs = Crs::from_epsg(target_epsg)?;
Self::new(source_crs, target_crs)
}
pub fn source_crs(&self) -> &Crs {
&self.source_crs
}
pub fn target_crs(&self) -> &Crs {
&self.target_crs
}
pub fn transform(&self, coord: &Coordinate) -> Result<Coordinate> {
self.check_area_of_use(coord.x, coord.y)?;
if self.proj.is_none() {
return Ok(*coord);
}
if !coord.is_valid() {
return Err(Error::invalid_coordinate(
"Coordinate contains non-finite values",
));
}
if self.strict && self.source_crs.is_geographic() {
if let Some(aou) = self.source_crs.area_of_use() {
if !aou.contains(coord.x, coord.y) {
return Err(Error::out_of_area_of_use(
coord.x,
coord.y,
self.source_crs.to_string(),
));
}
}
}
self.transform_impl(coord)
}
pub fn transform_3d(&self, coord: &Coordinate3D) -> Result<Coordinate3D> {
if let (Some((params, ref_epoch)), Some(t0), Some(t1)) =
(&self.itrf_params, self.source_epoch, self.target_epoch)
{
if !coord.is_valid() {
return Err(Error::invalid_coordinate(
"Coordinate contains non-finite values",
));
}
if (t1 - t0).abs() < f64::EPSILON {
return Ok(*coord);
}
let lat_rad = coord.y.to_radians();
let lon_rad = coord.x.to_radians();
let ellipsoid = crate::datum_transform::Ellipsoid::GRS80;
let bw_t1 = params.params_at_epoch(t1, *ref_epoch);
let bw_t0 = params.params_at_epoch(t0, *ref_epoch);
let net_bw = crate::datum_transform::BursaWolfParams {
tx: bw_t1.tx - bw_t0.tx,
ty: bw_t1.ty - bw_t0.ty,
tz: bw_t1.tz - bw_t0.tz,
rx: bw_t1.rx - bw_t0.rx,
ry: bw_t1.ry - bw_t0.ry,
rz: bw_t1.rz - bw_t0.rz,
ds: bw_t1.ds - bw_t0.ds,
};
let (lat_out_rad, lon_out_rad, h_out) =
net_bw.transform_geodetic(lat_rad, lon_rad, coord.z, &ellipsoid, &ellipsoid);
return Ok(Coordinate3D::new(
lon_out_rad.to_degrees(),
lat_out_rad.to_degrees(),
h_out,
));
}
if let (
CrsSource::Compound {
horizontal: src_h,
vertical: src_v,
},
CrsSource::Compound {
horizontal: dst_h,
vertical: dst_v,
},
) = (self.source_crs.source(), self.target_crs.source())
{
if !coord.is_valid() {
return Err(Error::invalid_coordinate(
"Coordinate contains non-finite values",
));
}
let h_transformer = Transformer::new((**src_h).clone(), (**dst_h).clone())?;
let xy_2d = Coordinate::new(coord.x, coord.y);
let transformed_xy = h_transformer.transform(&xy_2d)?;
let z = if src_v.is_equivalent(dst_v) {
coord.z
} else {
use crate::geoid::{VerticalDatumKind, classify_vertical_datum};
let src_kind = classify_vertical_datum(src_v.name().unwrap_or(""));
let dst_kind = classify_vertical_datum(dst_v.name().unwrap_or(""));
match (src_kind, dst_kind, self.geoid.as_ref()) {
(
VerticalDatumKind::Orthometric,
VerticalDatumKind::Ellipsoidal,
Some(grid),
) => grid.orthometric_to_ellipsoidal(coord.y, coord.x, coord.z),
(
VerticalDatumKind::Ellipsoidal,
VerticalDatumKind::Orthometric,
Some(grid),
) => grid.ellipsoidal_to_orthometric(coord.y, coord.x, coord.z),
_ => coord.z,
}
};
return Ok(Coordinate3D::new(transformed_xy.x, transformed_xy.y, z));
}
if self.proj.is_none() {
return Ok(*coord);
}
if !coord.is_valid() {
return Err(Error::invalid_coordinate(
"Coordinate contains non-finite values",
));
}
let coord_2d = coord.to_2d();
let transformed_2d = self.transform_impl(&coord_2d)?;
Ok(Coordinate3D::new(
transformed_2d.x,
transformed_2d.y,
coord.z,
))
}
pub fn transform_batch(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
if self.area_of_use_check != AreaOfUseCheck::Off {
for c in coords {
self.check_area_of_use(c.x, c.y)?;
}
}
if let Some(result) = self.try_simd_batch(coords) {
return result;
}
coords.iter().map(|c| self.transform(c)).collect()
}
fn try_simd_batch(&self, coords: &[Coordinate]) -> Option<Result<Vec<Coordinate>>> {
if coords.is_empty() {
return Some(Ok(Vec::new()));
}
if !self.source_crs.is_geographic() || !self.target_crs.is_projected() {
return None;
}
let proj_str = match self.target_crs.to_proj_string() {
Ok(s) => s,
Err(_) => return None,
};
let parsed = match ProjString::parse(&proj_str) {
Ok(p) => p,
Err(_) => return None,
};
let proj_type = parsed.proj()?;
match proj_type {
"tmerc" | "utm" => Some(self.simd_tmerc_forward(coords, &parsed)),
"merc" => Some(self.simd_merc_forward(coords, &parsed)),
"lcc" => Some(self.simd_lcc_forward(coords, &parsed)),
_ => None,
}
}
fn simd_tmerc_forward(
&self,
coords: &[Coordinate],
parsed: &ProjString,
) -> Result<Vec<Coordinate>> {
use simd::{WGS84_A, WGS84_E2, tmerc_forward_batch};
let proj_type = parsed.proj().unwrap_or("tmerc");
let (lon0_rad, k0, false_easting, false_northing, a, e2) = if proj_type == "utm" {
let zone = parsed.zone().unwrap_or(32) as f64;
let lon0_deg = zone * 6.0 - 183.0;
let false_northing = if parsed.has("south") {
10_000_000.0
} else {
0.0
};
(
lon0_deg.to_radians(),
0.9996,
500_000.0,
false_northing,
WGS84_A,
WGS84_E2,
)
} else {
let lon0_deg = parsed
.get("lon_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let k0 = parsed
.get("k")
.or_else(|| parsed.get("k_0"))
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(1.0);
let fe = parsed
.get("x_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let fn_ = parsed
.get("y_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let (a, e2) = parse_ellipsoid(parsed);
(lon0_deg.to_radians(), k0, fe, fn_, a, e2)
};
let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
let (xs, ys) = tmerc_forward_batch(
&lons,
&lats,
k0,
lon0_rad,
false_easting,
false_northing,
a,
e2,
);
let result: Vec<Coordinate> = xs
.into_iter()
.zip(ys)
.map(|(x, y)| {
let c = Coordinate::new(x, y);
if c.is_valid() {
Ok(c)
} else {
Err(Error::transformation_error(
"tmerc_batch: non-finite result",
))
}
})
.collect::<Result<Vec<_>>>()?;
Ok(result)
}
fn simd_merc_forward(
&self,
coords: &[Coordinate],
parsed: &ProjString,
) -> Result<Vec<Coordinate>> {
use simd::{WGS84_A, WGS84_E, merc_forward_batch};
let lon0_deg = parsed
.get("lon_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let k0 = parsed
.get("k")
.or_else(|| parsed.get("k_0"))
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(1.0);
let (a, e2) = parse_ellipsoid(parsed);
let e = e2.sqrt();
let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
let e_eff = if e < 1e-10 { 0.0 } else { e };
let _ = (WGS84_E, WGS84_A);
let (xs, ys) = merc_forward_batch(&lons, &lats, lon0_deg.to_radians(), k0, a, e_eff);
let result: Vec<Coordinate> = xs
.into_iter()
.zip(ys)
.map(|(x, y)| {
let c = Coordinate::new(x, y);
if c.is_valid() {
Ok(c)
} else {
Err(Error::transformation_error("merc_batch: non-finite result"))
}
})
.collect::<Result<Vec<_>>>()?;
Ok(result)
}
fn simd_lcc_forward(
&self,
coords: &[Coordinate],
parsed: &ProjString,
) -> Result<Vec<Coordinate>> {
use simd::{WGS84_A, lcc_cone_params, lcc_forward_batch};
let lon0_deg = parsed
.get("lon_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let lat0_deg = parsed
.get("lat_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let lat1_deg = parsed
.get("lat_1")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(lat0_deg);
let lat2_deg = parsed
.get("lat_2")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(lat1_deg);
let fe = parsed
.get("x_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let fn_ = parsed
.get("y_0")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let (a, _e2) = parse_ellipsoid(parsed);
let _ = WGS84_A;
let lat0_rad = lat0_deg.to_radians();
let lat1_rad = lat1_deg.to_radians();
let lat2_rad = lat2_deg.to_radians();
let (n, big_f, rho0_normalised) = match lcc_cone_params(lat0_rad, lat1_rad, lat2_rad) {
Some(p) => p,
None => {
return Err(Error::projection_init_error(
"lcc_batch: degenerate cone constant",
));
}
};
let rho0 = rho0_normalised * a;
let lons: Vec<f64> = coords.iter().map(|c| c.x.to_radians()).collect();
let lats: Vec<f64> = coords.iter().map(|c| c.y.to_radians()).collect();
let (xs, ys) = lcc_forward_batch(
&lons,
&lats,
n,
big_f,
rho0,
lon0_deg.to_radians(),
fe,
fn_,
a,
);
let result: Vec<Coordinate> = xs
.into_iter()
.zip(ys)
.map(|(x, y)| {
let c = Coordinate::new(x, y);
if c.is_valid() {
Ok(c)
} else {
Err(Error::transformation_error("lcc_batch: non-finite result"))
}
})
.collect::<Result<Vec<_>>>()?;
Ok(result)
}
pub fn transform_bbox(&self, bbox: &BoundingBox) -> Result<BoundingBox> {
if self.proj.is_none() {
return Ok(*bbox);
}
let corners = bbox.corners();
let transformed_corners = self.transform_batch(&corners)?;
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for corner in &transformed_corners {
min_x = min_x.min(corner.x);
min_y = min_y.min(corner.y);
max_x = max_x.max(corner.x);
max_y = max_y.max(corner.y);
}
BoundingBox::new(min_x, min_y, max_x, max_y)
}
fn transform_impl(&self, coord: &Coordinate) -> Result<Coordinate> {
let source_proj_str = self.source_crs.to_proj_string()?;
let target_proj_str = self.target_crs.to_proj_string()?;
let source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
.map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
.map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
let mut x = coord.x;
let mut y = coord.y;
if self.source_crs.is_geographic() {
x = x.to_radians();
y = y.to_radians();
}
let mut points = [(x, y)];
proj4rs::transform::transform(&source_proj, &target_proj, &mut points[..])
.map_err(|e| Error::transformation_error(format!("{:?}", e)))?;
let (mut result_x, mut result_y) = points[0];
if self.target_crs.is_geographic() {
result_x = result_x.to_degrees();
result_y = result_y.to_degrees();
}
let transformed = Coordinate::new(result_x, result_y);
if !transformed.is_valid() {
return Err(Error::transformation_error(
"Transformation resulted in non-finite values",
));
}
Ok(transformed)
}
}
#[cfg(feature = "std")]
fn parse_ellipsoid(parsed: &ProjString) -> (f64, f64) {
use simd::{WGS84_A, WGS84_E2};
if let Some(a_val) = parsed.get("a").and_then(|s| s.parse::<f64>().ok()) {
if let Some(b_val) = parsed.get("b").and_then(|s| s.parse::<f64>().ok()) {
let f = 1.0 - b_val / a_val;
let e2 = 2.0 * f - f * f;
return (a_val, e2);
}
if let Some(f_val) = parsed.get("f").and_then(|s| s.parse::<f64>().ok()) {
let e2 = 2.0 * f_val - f_val * f_val;
return (a_val, e2);
}
if let Some(rf) = parsed.get("rf").and_then(|s| s.parse::<f64>().ok()) {
let f = 1.0 / rf;
let e2 = 2.0 * f - f * f;
return (a_val, e2);
}
return (a_val, 0.0);
}
if let Some(ellps) = parsed.get("ellps") {
match ellps {
"WGS84" | "wgs84" => return (WGS84_A, WGS84_E2),
"GRS80" | "grs80" => {
let a = 6_378_137.0_f64;
let f = 1.0_f64 / 298.257_222_101;
let e2 = 2.0 * f - f * f;
return (a, e2);
}
"bessel" => {
let a = 6_377_397.155_f64;
let f = 1.0_f64 / 299.152_812_8;
let e2 = 2.0 * f - f * f;
return (a, e2);
}
"airy" => {
let a = 6_377_563.396_f64;
let b = 6_356_256.910_f64;
let f = 1.0 - b / a;
let e2 = 2.0 * f - f * f;
return (a, e2);
}
_ => {}
}
}
if let Some(datum) = parsed.get("datum") {
match datum {
"WGS84" | "wgs84" => return (WGS84_A, WGS84_E2),
"NAD83" | "nad83" => {
let a = 6_378_137.0_f64;
let f = 1.0_f64 / 298.257_222_101;
let e2 = 2.0 * f - f * f;
return (a, e2);
}
_ => {}
}
}
(WGS84_A, WGS84_E2)
}
#[cfg(feature = "std")]
pub fn transform_coordinate(
coord: &Coordinate,
source_crs: &Crs,
target_crs: &Crs,
) -> Result<Coordinate> {
let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
transformer.transform(coord)
}
#[cfg(feature = "std")]
pub fn transform_epsg(
coord: &Coordinate,
source_epsg: u32,
target_epsg: u32,
) -> Result<Coordinate> {
let transformer = Transformer::from_epsg(source_epsg, target_epsg)?;
transformer.transform(coord)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_coordinate_creation() {
let coord = Coordinate::new(10.0, 20.0);
assert_eq!(coord.x, 10.0);
assert_eq!(coord.y, 20.0);
}
#[test]
fn test_coordinate_from_lon_lat() {
let coord = Coordinate::from_lon_lat(-122.4194, 37.7749);
assert_eq!(coord.lon(), -122.4194);
assert_eq!(coord.lat(), 37.7749);
}
#[test]
fn test_coordinate_validation() {
let valid = Coordinate::new(0.0, 0.0);
assert!(valid.validate_geographic().is_ok());
let invalid_lon = Coordinate::new(200.0, 0.0);
assert!(invalid_lon.validate_geographic().is_err());
let invalid_lat = Coordinate::new(0.0, 100.0);
assert!(invalid_lat.validate_geographic().is_err());
}
#[test]
fn test_coordinate_is_valid() {
let valid = Coordinate::new(1.0, 2.0);
assert!(valid.is_valid());
let invalid = Coordinate::new(f64::NAN, 2.0);
assert!(!invalid.is_valid());
let infinite = Coordinate::new(f64::INFINITY, 2.0);
assert!(!infinite.is_valid());
}
#[test]
fn test_coordinate3d() {
let coord = Coordinate3D::new(1.0, 2.0, 3.0);
assert_eq!(coord.x, 1.0);
assert_eq!(coord.y, 2.0);
assert_eq!(coord.z, 3.0);
let coord_2d = coord.to_2d();
assert_eq!(coord_2d.x, 1.0);
assert_eq!(coord_2d.y, 2.0);
}
#[test]
fn test_bounding_box() {
let bbox = BoundingBox::new(0.0, 0.0, 10.0, 20.0);
assert!(bbox.is_ok());
let bbox = bbox.expect("should be valid");
assert_eq!(bbox.width(), 10.0);
assert_eq!(bbox.height(), 20.0);
let center = bbox.center();
assert_eq!(center.x, 5.0);
assert_eq!(center.y, 10.0);
}
#[test]
fn test_bounding_box_invalid() {
let result = BoundingBox::new(10.0, 0.0, 0.0, 20.0);
assert!(result.is_err());
let result = BoundingBox::new(0.0, 20.0, 10.0, 0.0);
assert!(result.is_err());
}
#[test]
fn test_bounding_box_contains() {
let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
assert!(bbox.contains(&Coordinate::new(5.0, 5.0)));
assert!(bbox.contains(&Coordinate::new(0.0, 0.0)));
assert!(bbox.contains(&Coordinate::new(10.0, 10.0)));
assert!(!bbox.contains(&Coordinate::new(-1.0, 5.0)));
assert!(!bbox.contains(&Coordinate::new(5.0, 11.0)));
}
#[test]
fn test_bounding_box_expand() {
let mut bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
bbox.expand_to_include(&Coordinate::new(15.0, 5.0));
assert_eq!(bbox.max_x, 15.0);
bbox.expand_to_include(&Coordinate::new(5.0, -5.0));
assert_eq!(bbox.min_y, -5.0);
}
#[test]
fn test_transformer_same_crs() {
let wgs84 = Crs::wgs84();
let transformer = Transformer::new(wgs84.clone(), wgs84.clone());
assert!(transformer.is_ok());
let transformer = transformer.expect("should create transformer");
let coord = Coordinate::new(10.0, 20.0);
let result = transformer.transform(&coord);
assert!(result.is_ok());
let result = result.expect("should transform");
assert_eq!(result, coord);
}
#[test]
fn test_transformer_wgs84_to_web_mercator() {
let transformer = Transformer::from_epsg(4326, 3857);
assert!(transformer.is_ok());
let transformer = transformer.expect("should create transformer");
let london = Coordinate::from_lon_lat(0.0, 51.5);
let result = transformer.transform(&london);
assert!(result.is_ok());
let result = result.expect("should transform");
assert_relative_eq!(result.x, 0.0, epsilon = 1.0);
assert!(result.y > 6_000_000.0 && result.y < 7_000_000.0);
}
#[test]
fn test_transform_batch() {
let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
let coords = vec![
Coordinate::new(0.0, 0.0),
Coordinate::new(10.0, 10.0),
Coordinate::new(20.0, 20.0),
];
let result = transformer.transform_batch(&coords);
assert!(result.is_ok());
let result = result.expect("should transform");
assert_eq!(result.len(), 3);
assert_eq!(result[0], coords[0]);
assert_eq!(result[1], coords[1]);
assert_eq!(result[2], coords[2]);
}
#[test]
fn test_transform_bbox() {
let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
let result = transformer.transform_bbox(&bbox);
assert!(result.is_ok());
let result = result.expect("should transform");
assert_eq!(result, bbox);
}
#[test]
fn test_convenience_functions() {
let wgs84 = Crs::wgs84();
let coord = Coordinate::new(0.0, 0.0);
let result = transform_coordinate(&coord, &wgs84, &wgs84);
assert!(result.is_ok());
assert_eq!(result.expect("should transform"), coord);
let result = transform_epsg(&coord, 4326, 4326);
assert!(result.is_ok());
assert_eq!(result.expect("should transform"), coord);
}
#[test]
fn test_transform_invalid_coordinate() {
let transformer = Transformer::from_epsg(4326, 3857).expect("should create");
let invalid = Coordinate::new(f64::NAN, 0.0);
let result = transformer.transform(&invalid);
assert!(result.is_err());
}
fn make_compound_wgs84_egm96() -> crate::crs::Crs {
let horiz = Crs::wgs84(); let vert_wkt = r#"VERTCRS["EGM96 height",VDATUM["EGM96 geoid"],UNIT["metre",1]]"#;
let vert = crate::crs::Crs::from_wkt(vert_wkt).expect("vert parse");
crate::crs::Crs::compound(horiz, vert).expect("compound CRS should build")
}
#[test]
fn test_transform_3d_compound_same_vertical_datum_passes_z_through() {
let crs = make_compound_wgs84_egm96();
let transformer = Transformer::new(crs.clone(), crs).expect("same-CRS transformer");
let input = Coordinate3D::new(13.4050, 52.5200, 34.567);
let output = transformer
.transform_3d(&input)
.expect("transform should succeed");
assert!((output.x - input.x).abs() < 1e-9, "x should be unchanged");
assert!((output.y - input.y).abs() < 1e-9, "y should be unchanged");
assert!(
(output.z - input.z).abs() < 1e-9,
"z should be passed through"
);
}
#[test]
fn test_transform_3d_compound_different_vertical_datum_silently_passes_through_when_no_geoid() {
let horiz = Crs::wgs84();
let vert1_wkt = r#"VERTCRS["EGM96 height",VDATUM["EGM96 geoid"],UNIT["metre",1]]"#;
let vert2_wkt = r#"VERTCRS["EGM2008 height",VDATUM["EGM2008 geoid"],UNIT["metre",1]]"#;
let vert1 = Crs::from_wkt(vert1_wkt).expect("vert1 parse");
let vert2 = Crs::from_wkt(vert2_wkt).expect("vert2 parse");
let crs1 = Crs::compound(horiz.clone(), vert1).expect("compound crs1");
let crs2 = Crs::compound(horiz, vert2).expect("compound crs2");
let transformer = Transformer::new(crs1, crs2).expect("different-vertical transformer");
let input = Coordinate3D::new(0.0, 51.5, 50.0);
let output = transformer
.transform_3d(&input)
.expect("must succeed without geoid (silent passthrough)");
assert!((output.x - input.x).abs() < 1e-9);
assert!((output.y - input.y).abs() < 1e-9);
assert!(
(output.z - input.z).abs() < 1e-9,
"z must pass through when no geoid attached"
);
}
#[test]
fn test_transform_3d_simple_non_compound_crs_unaffected() {
let transformer = Transformer::from_epsg(4326, 4326).expect("same EPSG");
let input = Coordinate3D::new(10.0, 50.0, 100.0);
let output = transformer.transform_3d(&input).expect("should transform");
assert!((output.x - input.x).abs() < 1e-9);
assert!((output.y - input.y).abs() < 1e-9);
assert!((output.z - input.z).abs() < 1e-9);
}
#[test]
fn test_transform_3d_compound_to_horizontal_still_works() {
let non_compound = Crs::wgs84();
let transformer = Transformer::new(non_compound.clone(), non_compound).expect("same CRS");
let input = Coordinate3D::new(5.0, 45.0, 200.0);
let output = transformer.transform_3d(&input).expect("should transform");
assert!((output.x - input.x).abs() < 1e-9);
assert!((output.y - input.y).abs() < 1e-9);
assert!((output.z - input.z).abs() < 1e-9);
}
}