use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, IoUnits, Operation, ProjError, ProjResult};
#[derive(Debug)]
struct AxisSwap {
axis: [usize; 4],
sign: [f64; 4],
}
impl Operation for AxisSwap {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let mut out = [0.0f64; 4];
for (i, slot) in out.iter_mut().enumerate() {
*slot = v[self.axis[i]] * self.sign[i];
}
Ok(Coord::new(out[0], out[1], out[2], out[3]))
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let mut out = [0.0f64; 4];
for ((src, &dst_idx), &s) in v.iter().zip(self.axis.iter()).zip(self.sign.iter()) {
out[dst_idx] = src * s;
}
Ok(Coord::new(out[0], out[1], out[2], out[3]))
}
fn has_inverse(&self) -> bool {
true
}
}
fn parse_order(s: &str) -> ProjResult<([usize; 4], [f64; 4])> {
let mut tokens: Vec<i32> = Vec::new();
for raw in s.split(',') {
let tok = raw.trim();
if tok.is_empty() {
return Err(ProjError::IllegalArgValue);
}
match tok.parse::<i32>() {
Ok(value) => tokens.push(value),
Err(_) => return Err(ProjError::IllegalArgValue),
}
}
let n = tokens.len();
if !(2..=4).contains(&n) {
return Err(ProjError::IllegalArgValue);
}
let mut axis = [0usize, 1, 2, 3];
let mut sign = [1.0f64; 4];
for (i, &tok) in tokens.iter().enumerate() {
if tok == 0 || !(1..=4).contains(&tok.abs()) {
return Err(ProjError::IllegalArgValue);
}
let idx = (tok.unsigned_abs() as usize) - 1;
axis[i] = idx;
sign[i] = if tok < 0 { -1.0 } else { 1.0 };
}
let mut seen = [false; 4];
for &a in axis.iter() {
if seen[a] {
return Err(ProjError::IllegalArgValue);
}
seen[a] = true;
}
Ok((axis, sign))
}
fn parse_axis(s: &str) -> ProjResult<([usize; 4], [f64; 4])> {
let bytes = s.as_bytes();
if bytes.len() != 3 {
return Err(ProjError::IllegalArgValue);
}
let mut axis = [0usize, 1, 2, 3];
let mut sign = [1.0f64; 4];
for (i, &b) in bytes.iter().enumerate() {
let (ax, sg) = match b {
b'e' => (0usize, 1.0f64),
b'w' => (0, -1.0),
b'n' => (1, 1.0),
b's' => (1, -1.0),
b'u' => (2, 1.0),
b'd' => (2, -1.0),
_ => return Err(ProjError::IllegalArgValue),
};
axis[i] = ax;
sign[i] = sg;
}
let mut seen = [false; 4];
for &a in &axis[..3] {
if seen[a] {
return Err(ProjError::IllegalArgValue);
}
seen[a] = true;
}
Ok((axis, sign))
}
pub fn new(p: &TransParams) -> ProjResult<TransBuild> {
let has_order = p.params.exists("order");
let has_axis = p.params.exists("axis");
if has_order == has_axis {
return Err(ProjError::MissingArg);
}
let (axis, sign) = if has_order {
let order = p.params.get_str("order").ok_or(ProjError::MissingArg)?;
parse_order(order)?
} else {
let axis_str = p.params.get_str("axis").ok_or(ProjError::MissingArg)?;
parse_axis(axis_str)?
};
Ok(TransBuild::new(
Box::new(AxisSwap { axis, sign }),
IoUnits::Whatever,
IoUnits::Whatever,
))
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::Coord;
struct OrderParams {
order: Option<String>,
}
impl crate::TransParamLookup for OrderParams {
fn get_dms(&self, _key: &str) -> Option<f64> {
None
}
fn get_f64(&self, _key: &str) -> Option<f64> {
None
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, key: &str) -> Option<&str> {
if key == "order" {
self.order.as_deref()
} else {
None
}
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, key: &str) -> bool {
key == "order" && self.order.is_some()
}
}
fn build_with(order: Option<&str>) -> ProjResult<TransBuild> {
let ell = oxiproj_core::Ellipsoid::named("WGS84").unwrap();
let params = OrderParams {
order: order.map(|s| s.to_string()),
};
let tp = TransParams {
ellipsoid: &ell,
params: ¶ms,
registry: None,
};
new(&tp)
}
struct AxisParams {
axis: Option<String>,
}
impl crate::TransParamLookup for AxisParams {
fn get_dms(&self, _key: &str) -> Option<f64> {
None
}
fn get_f64(&self, _key: &str) -> Option<f64> {
None
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, key: &str) -> Option<&str> {
if key == "axis" {
self.axis.as_deref()
} else {
None
}
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, key: &str) -> bool {
key == "axis" && self.axis.is_some()
}
}
fn build_with_axis(axis: Option<&str>) -> ProjResult<TransBuild> {
let ell = oxiproj_core::Ellipsoid::named("WGS84").unwrap();
let params = AxisParams {
axis: axis.map(|s| s.to_string()),
};
let tp = TransParams {
ellipsoid: &ell,
params: ¶ms,
registry: None,
};
new(&tp)
}
#[test]
fn order_swap_two() {
let build = build_with(Some("2,1")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(12.0, 55.0, 100.0, 0.0)).unwrap();
assert_eq!(fwd.v(), [55.0, 12.0, 100.0, 0.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [12.0, 55.0, 100.0, 0.0]);
}
#[test]
fn order_negate_second() {
let build = build_with(Some("1,-2,3,4")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(12.0, 55.0, 100.0, 0.0)).unwrap();
assert_eq!(fwd.v(), [12.0, -55.0, 100.0, 0.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [12.0, 55.0, 100.0, 0.0]);
}
#[test]
fn order_full_reverse() {
let build = build_with(Some("4,3,2,1")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(1.0, 2.0, 3.0, 4.0)).unwrap();
assert_eq!(fwd.v(), [4.0, 3.0, 2.0, 1.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn missing_order_is_missing_arg() {
assert_eq!(build_with(None).err(), Some(ProjError::MissingArg));
}
#[test]
fn out_of_range_is_illegal() {
assert_eq!(
build_with(Some("5,1")).err(),
Some(ProjError::IllegalArgValue)
);
}
#[test]
fn duplicate_axis_is_illegal() {
assert_eq!(
build_with(Some("1,1")).err(),
Some(ProjError::IllegalArgValue)
);
}
#[test]
fn axis_enu_is_identity() {
let build = build_with_axis(Some("enu")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(1.0, 2.0, 3.0, 0.0)).unwrap();
assert_eq!(fwd.v(), [1.0, 2.0, 3.0, 0.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [1.0, 2.0, 3.0, 0.0]);
}
#[test]
fn axis_neu_swaps_x_y() {
let build = build_with_axis(Some("neu")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(12.0, 55.0, 100.0, 0.0)).unwrap();
assert_eq!(fwd.v(), [55.0, 12.0, 100.0, 0.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [12.0, 55.0, 100.0, 0.0]);
}
#[test]
fn axis_wsu_negates_x_and_y() {
let build = build_with_axis(Some("wsu")).unwrap();
let op = build.operation;
let fwd = op.forward_4d(Coord::new(12.0, 55.0, 100.0, 0.0)).unwrap();
assert_eq!(fwd.v(), [-12.0, -55.0, 100.0, 0.0]);
let inv = op.inverse_4d(fwd).unwrap();
assert_eq!(inv.v(), [12.0, 55.0, 100.0, 0.0]);
}
#[test]
fn axis_and_order_both_absent_is_missing_arg() {
let build = build_with_axis(None);
assert_eq!(build.err(), Some(ProjError::MissingArg));
}
#[test]
fn axis_invalid_char_is_illegal() {
assert_eq!(
build_with_axis(Some("exu")).err(),
Some(ProjError::IllegalArgValue)
);
}
#[test]
fn axis_wrong_length_is_illegal() {
assert_eq!(
build_with_axis(Some("en")).err(),
Some(ProjError::IllegalArgValue)
);
assert_eq!(
build_with_axis(Some("enud")).err(),
Some(ProjError::IllegalArgValue)
);
}
#[test]
fn axis_duplicate_is_illegal() {
assert_eq!(
build_with_axis(Some("een")).err(),
Some(ProjError::IllegalArgValue)
);
}
}