use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::proj_string::ProjString;
use crate::transform::Coordinate;
pub type ParsedStep = (HashMap<String, Option<String>>, bool);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
M,
Km,
Ft,
USFt,
Rad,
Deg,
}
impl Unit {
pub fn to_meters_per_unit(self) -> f64 {
match self {
Self::M => 1.0,
Self::Km => 1_000.0,
Self::Ft => 0.3048,
Self::USFt => 0.304_800_609_601_219_24,
Self::Rad | Self::Deg => 1.0,
}
}
pub fn to_radians_per_unit(self) -> f64 {
match self {
Self::Rad => 1.0,
Self::Deg => std::f64::consts::PI / 180.0,
Self::M | Self::Km | Self::Ft | Self::USFt => 1.0,
}
}
pub fn is_angular(self) -> bool {
matches!(self, Self::Rad | Self::Deg)
}
pub fn from_proj_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"m" | "metre" | "meters" | "metres" => Some(Self::M),
"km" | "kilometre" | "kilometer" | "kilometres" | "kilometers" => Some(Self::Km),
"ft" | "foot" | "feet" | "intl ft" => Some(Self::Ft),
"us-ft" | "us_ft" | "survey ft" | "survey_ft" | "us survey foot" => Some(Self::USFt),
"rad" | "radian" | "radians" => Some(Self::Rad),
"deg" | "degree" | "degrees" => Some(Self::Deg),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HelmertConvention {
PositionVector,
CoordinateFrame,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HelmertParams {
pub tx: f64,
pub ty: f64,
pub tz: f64,
pub rx: f64,
pub ry: f64,
pub rz: f64,
pub s: f64,
pub convention: HelmertConvention,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HelmertRateParams {
pub dtx: f64,
pub dty: f64,
pub dtz: f64,
pub drx: f64,
pub dry: f64,
pub drz: f64,
pub ds: f64,
pub ref_epoch: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EllipsoidParams {
pub a: f64,
pub f: f64,
}
impl EllipsoidParams {
#[inline]
pub fn b(&self) -> f64 {
self.a * (1.0 - self.f)
}
#[inline]
pub fn e2(&self) -> f64 {
let b = self.b();
1.0 - (b / self.a) * (b / self.a)
}
#[inline]
pub fn prime_vertical_radius(&self, lat_rad: f64) -> f64 {
let e2 = self.e2();
let sin_lat = lat_rad.sin();
self.a / (1.0 - e2 * sin_lat * sin_lat).sqrt()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ShiftDirection {
Forward,
Inverse,
}
#[derive(Debug, Clone)]
pub enum StepKind {
AxisSwap {
order: [u8; 4],
},
UnitConvert {
from: Unit,
to: Unit,
},
Project {
proj_string: String,
},
Passthrough,
Helmert {
params: HelmertParams,
},
Cart {
ellipsoid: EllipsoidParams,
},
Vgridshift {
grid_id: String,
direction: ShiftDirection,
},
Hgridshift {
grid_id: String,
direction: ShiftDirection,
},
HelmertTemporal {
params: HelmertParams,
rates: HelmertRateParams,
epoch: f64,
},
}
#[derive(Debug, Clone)]
pub struct PipelineStep {
pub kind: StepKind,
pub inverse: bool,
}
impl PipelineStep {
pub fn new(kind: StepKind) -> Self {
Self {
kind,
inverse: false,
}
}
pub fn new_inverse(kind: StepKind) -> Self {
Self {
kind,
inverse: true,
}
}
pub fn apply(&self, coord: &Coordinate) -> Result<Coordinate> {
apply_step_kind(&self.kind, coord, self.inverse)
}
pub fn apply_flipped(&self, coord: &Coordinate) -> Result<Coordinate> {
apply_step_kind(&self.kind, coord, !self.inverse)
}
}
#[derive(Debug, Clone)]
pub struct Pipeline {
steps: Vec<PipelineStep>,
inverse: bool,
}
impl Default for Pipeline {
fn default() -> Self {
Self::new()
}
}
impl Pipeline {
pub fn new() -> Self {
Self {
steps: Vec::new(),
inverse: false,
}
}
pub fn step(mut self, kind: StepKind) -> Self {
self.steps.push(PipelineStep::new(kind));
self
}
pub fn step_inv(mut self, kind: StepKind) -> Self {
self.steps.push(PipelineStep::new_inverse(kind));
self
}
pub fn with_inverse(mut self, inv: bool) -> Self {
self.inverse = inv;
self
}
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn is_inverse(&self) -> bool {
self.inverse
}
pub fn transform(&self, coord: &Coordinate) -> Result<Coordinate> {
let mut current = *coord;
if self.inverse {
for (idx, step) in self.steps.iter().enumerate().rev() {
current = step
.apply_flipped(¤t)
.map_err(|e| Error::PipelineStepError {
step: idx,
inner: format!("{}", e),
})?;
}
} else {
for (idx, step) in self.steps.iter().enumerate() {
current = step.apply(¤t).map_err(|e| Error::PipelineStepError {
step: idx,
inner: format!("{}", e),
})?;
}
}
Ok(current)
}
pub fn transform_many(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
coords.iter().map(|c| self.transform(c)).collect()
}
pub fn from_proj_string(s: &str) -> Result<Self> {
let step_chunks = parse_pipeline(s)?;
let global_inv = detect_global_inv(s);
let mut pipeline = Self::new().with_inverse(global_inv);
for (idx, (params, step_inv)) in step_chunks.into_iter().enumerate() {
let kind = params_to_step_kind(params, idx)?;
if step_inv {
pipeline.steps.push(PipelineStep::new_inverse(kind));
} else {
pipeline.steps.push(PipelineStep::new(kind));
}
}
Ok(pipeline)
}
}
fn detect_global_inv(s: &str) -> bool {
let header = if let Some(pos) = s.find("+step") {
&s[..pos]
} else {
s
};
for token in header.split_whitespace() {
if token == "+inv" {
return true;
}
}
false
}
fn apply_step_kind(kind: &StepKind, coord: &Coordinate, inverse: bool) -> Result<Coordinate> {
match kind {
StepKind::Passthrough => Ok(*coord),
StepKind::AxisSwap { order } => apply_axis_swap(coord, order, inverse),
StepKind::UnitConvert { from, to } => apply_unit_convert(coord, *from, *to, inverse),
StepKind::Project { proj_string } => apply_project(coord, proj_string, inverse),
StepKind::Helmert { params } => apply_helmert(coord, params, inverse),
StepKind::Cart { ellipsoid } => apply_cart(coord, ellipsoid, inverse),
StepKind::Vgridshift { grid_id, .. } => Err(Error::PipelineParseError(format!(
"vgridshift: grid '{}' not loaded",
grid_id
))),
StepKind::Hgridshift { grid_id, .. } => {
Err(Error::PipelineParseError(format!(
"hgridshift: grid '{}' not loaded",
grid_id
)))
}
StepKind::HelmertTemporal {
params,
rates,
epoch,
} => apply_helmert_temporal(coord, params, rates, *epoch, inverse),
}
}
fn apply_axis_swap(coord: &Coordinate, order: &[u8; 4], inverse: bool) -> Result<Coordinate> {
let active: Vec<u8> = order.iter().copied().filter(|&v| v != 0).collect();
let input = [coord.x, coord.y];
if active.is_empty() {
return Ok(*coord);
}
let effective_order: Vec<u8> = if inverse {
let n = active.len();
let mut inv_order = vec![0u8; n];
for (i, &k) in active.iter().enumerate() {
let ki = k as usize;
if ki == 0 || ki > n {
return Err(Error::PipelineParseError(format!(
"axis order value {} is out of range 1..={}",
k, n
)));
}
inv_order[ki - 1] = (i + 1) as u8;
}
inv_order
} else {
active.clone()
};
let n = effective_order.len().min(2);
let mut out_x = coord.x;
let mut out_y = coord.y;
if n >= 1 {
let src_idx = (effective_order[0] as usize).saturating_sub(1);
if src_idx < input.len() {
out_x = input[src_idx];
}
}
if n >= 2 {
let src_idx = (effective_order[1] as usize).saturating_sub(1);
if src_idx < input.len() {
out_y = input[src_idx];
}
}
Ok(Coordinate::new(out_x, out_y))
}
fn apply_unit_convert(
coord: &Coordinate,
from: Unit,
to: Unit,
inverse: bool,
) -> Result<Coordinate> {
let (actual_from, actual_to) = if inverse { (to, from) } else { (from, to) };
let scale = if actual_from.is_angular() || actual_to.is_angular() {
actual_from.to_radians_per_unit() / actual_to.to_radians_per_unit()
} else {
actual_from.to_meters_per_unit() / actual_to.to_meters_per_unit()
};
Ok(Coordinate::new(coord.x * scale, coord.y * scale))
}
fn apply_project(coord: &Coordinate, proj_string: &str, inverse: bool) -> Result<Coordinate> {
let geo_proj = proj4rs::Proj::from_proj_string("+proj=longlat +datum=WGS84")
.map_err(|e| Error::PipelineParseError(format!("geo proj init failed: {:?}", e)))?;
let step_proj = proj4rs::Proj::from_proj_string(proj_string)
.map_err(|e| Error::PipelineParseError(format!("step proj init failed: {:?}", e)))?;
let (src_proj, dst_proj, src_is_geo, dst_is_geo) = if inverse {
(&step_proj, &geo_proj, false, true)
} else {
(&geo_proj, &step_proj, true, false)
};
let mut x = coord.x;
let mut y = coord.y;
if src_is_geo {
x = x.to_radians();
y = y.to_radians();
}
let mut points = [(x, y)];
proj4rs::transform::transform(src_proj, dst_proj, &mut points[..])
.map_err(|e| Error::transformation_error(format!("{:?}", e)))?;
let (mut rx, mut ry) = points[0];
if dst_is_geo {
rx = rx.to_degrees();
ry = ry.to_degrees();
}
let result = Coordinate::new(rx, ry);
if !result.is_valid() {
return Err(Error::transformation_error(
"Project step produced non-finite coordinate",
));
}
Ok(result)
}
fn apply_helmert(coord: &Coordinate, params: &HelmertParams, inverse: bool) -> Result<Coordinate> {
let arc_sec_to_rad = std::f64::consts::PI / 648_000.0;
let drx_raw = params.rx * arc_sec_to_rad;
let dry_raw = params.ry * arc_sec_to_rad;
let drz_raw = params.rz * arc_sec_to_rad;
let (drx, dry, drz) = if params.convention == HelmertConvention::CoordinateFrame {
(-drx_raw, -dry_raw, -drz_raw)
} else {
(drx_raw, dry_raw, drz_raw)
};
let _drx = drx;
let _dry = dry;
let ds = params.s * 1.0e-6;
let scale = 1.0 + ds;
if !inverse {
let x = coord.x;
let y = coord.y;
let out_x = params.tx + scale * x + drz * y;
let out_y = params.ty - drz * x + scale * y;
Ok(Coordinate::new(out_x, out_y))
} else {
let dx = coord.x - params.tx;
let dy = coord.y - params.ty;
let det = scale * scale + drz * drz;
if det.abs() < 1e-30 {
return Err(Error::transformation_error(
"Helmert inverse: degenerate scale/rotation combination",
));
}
let out_x = (scale * dx - drz * dy) / det;
let out_y = (drz * dx + scale * dy) / det;
Ok(Coordinate::new(out_x, out_y))
}
}
fn apply_helmert_temporal(
coord: &Coordinate,
params: &HelmertParams,
rates: &HelmertRateParams,
epoch: f64,
inverse: bool,
) -> Result<Coordinate> {
let dt = epoch - rates.ref_epoch;
let effective = HelmertParams {
tx: params.tx + rates.dtx / 1_000.0 * dt,
ty: params.ty + rates.dty / 1_000.0 * dt,
tz: params.tz + rates.dtz / 1_000.0 * dt,
rx: params.rx + rates.drx / 1_000.0 * dt,
ry: params.ry + rates.dry / 1_000.0 * dt,
rz: params.rz + rates.drz / 1_000.0 * dt,
s: params.s + rates.ds / 1_000.0 * dt,
convention: params.convention.clone(),
};
apply_helmert(coord, &effective, inverse)
}
fn apply_cart(coord: &Coordinate, ell: &EllipsoidParams, inverse: bool) -> Result<Coordinate> {
if !inverse {
let lon_rad = coord.x.to_radians();
let lat_rad = coord.y.to_radians();
let h = 0.0_f64;
let n = ell.prime_vertical_radius(lat_rad);
let e2 = ell.e2();
let cos_lat = lat_rad.cos();
let sin_lat = lat_rad.sin();
let x_ecef = (n + h) * cos_lat * lon_rad.cos();
let y_ecef = (n + h) * cos_lat * lon_rad.sin();
let _z_ecef = (n * (1.0 - e2) + h) * sin_lat;
if !x_ecef.is_finite() || !y_ecef.is_finite() {
return Err(Error::transformation_error(
"Cart forward produced non-finite value",
));
}
Ok(Coordinate::new(x_ecef, y_ecef))
} else {
let x_ecef = coord.x;
let y_ecef = coord.y;
let z_ecef = 0.0_f64;
let a = ell.a;
let b = ell.b();
let e2 = ell.e2();
let ep2 = (a * a - b * b) / (b * b);
let p = (x_ecef * x_ecef + y_ecef * y_ecef).sqrt();
let lon_rad = y_ecef.atan2(x_ecef);
let theta = (z_ecef * a).atan2(p * b);
let sin_theta = theta.sin();
let cos_theta = theta.cos();
let mut lat_rad = (z_ecef + ep2 * b * sin_theta * sin_theta * sin_theta)
.atan2(p - e2 * a * cos_theta * cos_theta * cos_theta);
for _ in 0..3 {
let sin_lat = lat_rad.sin();
let n = a / (1.0 - e2 * sin_lat * sin_lat).sqrt();
lat_rad = (z_ecef + e2 * n * sin_lat).atan2(p);
}
if !lon_rad.is_finite() || !lat_rad.is_finite() {
return Err(Error::transformation_error(
"Cart inverse (Bowring) produced non-finite value",
));
}
Ok(Coordinate::new(lon_rad.to_degrees(), lat_rad.to_degrees()))
}
}
fn ellipsoid_from_name(name: &str) -> Option<EllipsoidParams> {
match name.to_lowercase().as_str() {
"wgs84" | "wgs_84" => Some(EllipsoidParams {
a: 6_378_137.0,
f: 1.0 / 298.257_223_563,
}),
"grs80" | "grs_80" => Some(EllipsoidParams {
a: 6_378_137.0,
f: 1.0 / 298.257_222_101,
}),
"bessel" => Some(EllipsoidParams {
a: 6_377_397.155,
f: 1.0 / 299.152_843_4,
}),
_ => None,
}
}
fn parse_helmert_step(
params: HashMap<String, Option<String>>,
step_idx: usize,
) -> Result<StepKind> {
let parse_f64 = |key: &str, default: f64| -> Result<f64> {
match params.get(key).and_then(|v| v.as_deref()) {
None => Ok(default),
Some(s) => s.parse::<f64>().map_err(|_| {
Error::PipelineParseError(format!(
"step {}: helmert +{} '{}' is not a valid f64",
step_idx, key, s
))
}),
}
};
let tx = parse_f64("x", 0.0)?;
let ty = parse_f64("y", 0.0)?;
let tz = parse_f64("z", 0.0)?;
let rx = parse_f64("rx", 0.0)?;
let ry = parse_f64("ry", 0.0)?;
let rz = parse_f64("rz", 0.0)?;
let s = parse_f64("s", 0.0)?;
let convention_str = params
.get("convention")
.and_then(|v| v.as_deref())
.unwrap_or("position_vector");
let convention = match convention_str.to_lowercase().as_str() {
"position_vector" | "bursa_wolf" | "bursawolf" => HelmertConvention::PositionVector,
"coordinate_frame" | "coordinateframe" => HelmertConvention::CoordinateFrame,
other => {
return Err(Error::PipelineParseError(format!(
"step {}: helmert unknown +convention '{}'",
step_idx, other
)));
}
};
Ok(StepKind::Helmert {
params: HelmertParams {
tx,
ty,
tz,
rx,
ry,
rz,
s,
convention,
},
})
}
fn parse_helmert_temporal_step(
params: HashMap<String, Option<String>>,
step_idx: usize,
) -> Result<StepKind> {
let parse_f64 = |key: &str, default: f64| -> Result<f64> {
match params.get(key).and_then(|v| v.as_deref()) {
None => Ok(default),
Some(s) => s.parse::<f64>().map_err(|_| {
Error::PipelineParseError(format!(
"step {}: helmert_temporal +{} '{}' is not a valid f64",
step_idx, key, s
))
}),
}
};
let tx = parse_f64("x", 0.0)?;
let ty = parse_f64("y", 0.0)?;
let tz = parse_f64("z", 0.0)?;
let rx = parse_f64("rx", 0.0)?;
let ry = parse_f64("ry", 0.0)?;
let rz = parse_f64("rz", 0.0)?;
let s = parse_f64("s", 0.0)?;
let convention_str = params
.get("convention")
.and_then(|v| v.as_deref())
.unwrap_or("position_vector");
let convention = match convention_str.to_lowercase().as_str() {
"position_vector" | "bursa_wolf" | "bursawolf" => HelmertConvention::PositionVector,
"coordinate_frame" | "coordinateframe" => HelmertConvention::CoordinateFrame,
other => {
return Err(Error::PipelineParseError(format!(
"step {}: helmert_temporal unknown +convention '{}'",
step_idx, other
)));
}
};
let dtx = parse_f64("dtx", 0.0)?;
let dty = parse_f64("dty", 0.0)?;
let dtz = parse_f64("dtz", 0.0)?;
let drx = parse_f64("drx", 0.0)?;
let dry = parse_f64("dry", 0.0)?;
let drz = parse_f64("drz", 0.0)?;
let ds_rate = parse_f64("ds", 0.0)?;
let ref_epoch = parse_f64("ref_epoch", 0.0)?;
let epoch = parse_f64("t_epoch", ref_epoch)?;
Ok(StepKind::HelmertTemporal {
params: HelmertParams {
tx,
ty,
tz,
rx,
ry,
rz,
s,
convention,
},
rates: HelmertRateParams {
dtx,
dty,
dtz,
drx,
dry,
drz,
ds: ds_rate,
ref_epoch,
},
epoch,
})
}
fn parse_cart_step(params: HashMap<String, Option<String>>, step_idx: usize) -> Result<StepKind> {
let a_opt = params
.get("a")
.and_then(|v| v.as_deref())
.and_then(|s| s.parse::<f64>().ok());
let rf_opt = params
.get("rf")
.and_then(|v| v.as_deref())
.and_then(|s| s.parse::<f64>().ok());
let ellipsoid = if let (Some(a), Some(rf)) = (a_opt, rf_opt) {
EllipsoidParams { a, f: 1.0 / rf }
} else {
let ellps_str = params
.get("ellps")
.and_then(|v| v.as_deref())
.unwrap_or("WGS84");
ellipsoid_from_name(ellps_str).ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: cart unknown +ellps '{}'",
step_idx, ellps_str
))
})?
};
Ok(StepKind::Cart { ellipsoid })
}
fn parse_vgridshift_step(
params: HashMap<String, Option<String>>,
step_idx: usize,
) -> Result<StepKind> {
let grid_id = params
.get("grids")
.and_then(|v| v.as_deref())
.ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: vgridshift missing +grids parameter",
step_idx
))
})?
.to_string();
let direction = parse_shift_direction(
params.get("direction").and_then(|v| v.as_deref()),
step_idx,
"vgridshift",
)?;
Ok(StepKind::Vgridshift { grid_id, direction })
}
fn parse_hgridshift_step(
params: HashMap<String, Option<String>>,
step_idx: usize,
) -> Result<StepKind> {
let grid_id = params
.get("grids")
.and_then(|v| v.as_deref())
.ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: hgridshift missing +grids parameter",
step_idx
))
})?
.to_string();
let direction = parse_shift_direction(
params.get("direction").and_then(|v| v.as_deref()),
step_idx,
"hgridshift",
)?;
Ok(StepKind::Hgridshift { grid_id, direction })
}
fn parse_shift_direction(
s: Option<&str>,
step_idx: usize,
step_name: &str,
) -> Result<ShiftDirection> {
match s.unwrap_or("forward").to_lowercase().as_str() {
"forward" | "fwd" => Ok(ShiftDirection::Forward),
"inverse" | "inv" => Ok(ShiftDirection::Inverse),
other => Err(Error::PipelineParseError(format!(
"step {}: {} unknown +direction '{}'",
step_idx, step_name, other
))),
}
}
fn params_to_step_kind(
params: HashMap<String, Option<String>>,
step_idx: usize,
) -> Result<StepKind> {
let proj_val = params.get("proj").and_then(|v| v.as_deref()).unwrap_or("");
match proj_val {
"axisswap" => {
let order_str = params
.get("order")
.and_then(|v| v.as_deref())
.ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: axisswap missing +order parameter",
step_idx
))
})?;
let order = parse_axis_order(order_str, step_idx)?;
Ok(StepKind::AxisSwap { order })
}
"unitconvert" => {
let from_str = params
.get("xy_in")
.and_then(|v| v.as_deref())
.or_else(|| params.get("from").and_then(|v| v.as_deref()))
.ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: unitconvert missing +xy_in parameter",
step_idx
))
})?;
let to_str = params
.get("xy_out")
.and_then(|v| v.as_deref())
.or_else(|| params.get("to").and_then(|v| v.as_deref()))
.ok_or_else(|| {
Error::PipelineParseError(format!(
"step {}: unitconvert missing +xy_out parameter",
step_idx
))
})?;
let from = Unit::from_proj_str(from_str).ok_or_else(|| {
Error::PipelineParseError(format!("step {}: unknown unit '{}'", step_idx, from_str))
})?;
let to = Unit::from_proj_str(to_str).ok_or_else(|| {
Error::PipelineParseError(format!("step {}: unknown unit '{}'", step_idx, to_str))
})?;
Ok(StepKind::UnitConvert { from, to })
}
"passthrough" | "" => Ok(StepKind::Passthrough),
"helmert" => parse_helmert_step(params, step_idx),
"helmert_temporal" => parse_helmert_temporal_step(params, step_idx),
"cart" => parse_cart_step(params, step_idx),
"vgridshift" => parse_vgridshift_step(params, step_idx),
"hgridshift" => parse_hgridshift_step(params, step_idx),
_ => {
let ps = ProjString { params };
Ok(StepKind::Project {
proj_string: ps.to_proj_string(),
})
}
}
}
fn parse_axis_order(s: &str, step_idx: usize) -> Result<[u8; 4]> {
let mut order = [0u8; 4];
for (i, part) in s.split(',').enumerate() {
if i >= 4 {
return Err(Error::PipelineParseError(format!(
"step {}: axisswap order has more than 4 axes",
step_idx
)));
}
let v: u8 = part.trim().parse().map_err(|_| {
Error::PipelineParseError(format!(
"step {}: axisswap order '{}' is not a valid u8",
step_idx, part
))
})?;
order[i] = v;
}
Ok(order)
}
pub fn parse_pipeline(s: &str) -> Result<Vec<ParsedStep>> {
let s = s.trim();
if !s.contains("+proj=pipeline") {
return Err(Error::PipelineParseError(
"pipeline string must contain +proj=pipeline".to_string(),
));
}
let raw_chunks: Vec<&str> = s.split("+step").map(str::trim).collect();
let step_chunks = &raw_chunks[1..];
let mut result: Vec<ParsedStep> = Vec::with_capacity(step_chunks.len());
for (idx, chunk) in step_chunks.iter().enumerate() {
if chunk.is_empty() {
continue;
}
let step_inv = has_inv_flag(chunk);
let cleaned = strip_inv_flag(chunk);
if cleaned.trim().is_empty() {
result.push((HashMap::new(), step_inv));
continue;
}
let ps = ProjString::parse(cleaned.trim())
.map_err(|e| Error::PipelineParseError(format!("step {}: {}", idx, e)))?;
result.push((ps.params, step_inv));
}
Ok(result)
}
fn has_inv_flag(chunk: &str) -> bool {
for token in chunk.split_whitespace() {
if token == "+inv" {
return true;
}
}
false
}
fn strip_inv_flag(chunk: &str) -> String {
chunk
.split_whitespace()
.filter(|t| *t != "+inv")
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_unit_meters_per_unit() {
assert!((Unit::M.to_meters_per_unit() - 1.0).abs() < 1e-15);
assert!((Unit::Km.to_meters_per_unit() - 1000.0).abs() < 1e-15);
assert!((Unit::Ft.to_meters_per_unit() - 0.3048).abs() < 1e-15);
}
#[test]
fn test_unit_radians_per_unit() {
assert!((Unit::Rad.to_radians_per_unit() - 1.0).abs() < 1e-15);
let deg_rad = std::f64::consts::PI / 180.0;
assert!((Unit::Deg.to_radians_per_unit() - deg_rad).abs() < 1e-20);
}
#[test]
fn test_unit_from_proj_str() {
assert_eq!(Unit::from_proj_str("m"), Some(Unit::M));
assert_eq!(Unit::from_proj_str("km"), Some(Unit::Km));
assert_eq!(Unit::from_proj_str("ft"), Some(Unit::Ft));
assert_eq!(Unit::from_proj_str("us-ft"), Some(Unit::USFt));
assert_eq!(Unit::from_proj_str("rad"), Some(Unit::Rad));
assert_eq!(Unit::from_proj_str("deg"), Some(Unit::Deg));
assert_eq!(Unit::from_proj_str("parsec"), None);
}
#[test]
fn test_axis_swap_forward() {
let coord = Coordinate::new(10.0, 20.0);
let result =
apply_axis_swap(&coord, &[2, 1, 0, 0], false).expect("axisswap should succeed");
assert!((result.x - 20.0).abs() < 1e-10);
assert!((result.y - 10.0).abs() < 1e-10);
}
#[test]
fn test_axis_swap_inverse() {
let coord = Coordinate::new(10.0, 20.0);
let result =
apply_axis_swap(&coord, &[2, 1, 0, 0], true).expect("axisswap inv should succeed");
assert!((result.x - 20.0).abs() < 1e-10);
assert!((result.y - 10.0).abs() < 1e-10);
}
#[test]
fn test_unit_convert_m_to_km() {
let coord = Coordinate::new(1000.0, 2000.0);
let result = apply_unit_convert(&coord, Unit::M, Unit::Km, false).expect("unit convert ok");
assert!((result.x - 1.0).abs() < 1e-10);
assert!((result.y - 2.0).abs() < 1e-10);
}
#[test]
fn test_unit_convert_km_to_m_inverse() {
let coord = Coordinate::new(1.0, 2.0);
let result =
apply_unit_convert(&coord, Unit::M, Unit::Km, true).expect("unit convert inverse ok");
assert!((result.x - 1000.0).abs() < 1e-10);
assert!((result.y - 2000.0).abs() < 1e-10);
}
#[test]
fn test_parse_axis_order_valid() {
let order = parse_axis_order("2,1", 0).expect("valid order");
assert_eq!(order, [2, 1, 0, 0]);
}
#[test]
fn test_parse_pipeline_basic() {
let s = "+proj=pipeline +step +proj=axisswap +order=2,1";
let steps = parse_pipeline(s).expect("valid pipeline string");
assert_eq!(steps.len(), 1);
let (params, inv) = &steps[0];
assert!(!inv);
assert_eq!(
params.get("proj").and_then(|v| v.as_deref()),
Some("axisswap")
);
}
#[test]
fn test_parse_pipeline_step_inv() {
let s = "+proj=pipeline +step +inv +proj=axisswap +order=2,1";
let steps = parse_pipeline(s).expect("valid pipeline string");
assert_eq!(steps.len(), 1);
let (_params, inv) = &steps[0];
assert!(inv);
}
}