From 6b1b43b8da20b149de77a416f1932bd8f6af3dcf Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 12 Dec 2023 15:00:18 +0100 Subject: [PATCH 1/2] Initial conversion support with first operations being `axisswap` and `noop`. --- src/conversions/axisswap.rs | 283 ++++++++++++++++++++++++++++++++++ src/conversions/conversion.rs | 53 +++++++ src/conversions/convert.rs | 25 +++ src/conversions/mod.rs | 11 ++ src/conversions/noop.rs | 19 +++ src/lib.rs | 8 + src/transform.rs | 1 + 7 files changed, 400 insertions(+) create mode 100644 src/conversions/axisswap.rs create mode 100644 src/conversions/conversion.rs create mode 100644 src/conversions/convert.rs create mode 100644 src/conversions/mod.rs create mode 100644 src/conversions/noop.rs diff --git a/src/conversions/axisswap.rs b/src/conversions/axisswap.rs new file mode 100644 index 0000000..6a59417 --- /dev/null +++ b/src/conversions/axisswap.rs @@ -0,0 +1,283 @@ +//! Refernce: https://proj.org/en/9.3/operations/conversions/axisswap.html + +use crate::*; + +#[derive(Debug, Clone)] +pub struct AxisswapConversion { + ordering: AxisswapOrdering, +} + +impl Convert for AxisswapConversion { + const NAME: &'static str = "axisswap"; + type Parameters = AxisswapOrdering; + + fn new(ordering: Self::Parameters) -> ProjResult { + Ok(Self { ordering }) + } + + fn convert(&self, x: f64, y: f64, z: f64) -> ProjResult<(f64, f64, f64)> { + let output = self.ordering.apply_ordering([x, y, z]); + Ok((output[0], output[1], output[2])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn disallows_missing_order_parameter() { + assert!(Conversion::from_proj_string("+proj=axisswap").is_err(),) + } + + #[test] + fn converts_from_proj_string() { + let conversion = Conversion::from_proj_string("+proj=axisswap +order=2,1").unwrap(); + let mut points = (1., 2., 0.); + conversion.convert(&mut points).unwrap(); + assert_eq!((2., 1., 0.), points); + } +} + +pub use ordering::AxisswapOrdering; +mod ordering { + use std::str::FromStr; + + use super::*; + + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct Flip(bool); + + #[derive(Debug, Clone)] + #[cfg_attr(test, derive(PartialEq))] + pub struct AxisswapOrdering([(u8, Flip); 3]); + + impl ConvertParameters for AxisswapOrdering { + fn from_parameter_list(parameter_list: &ParamList) -> ProjResult { + parameter_list + .get("order") + .ok_or(ProjError::NoValueParameter)? + .value + .ok_or(ProjError::NoValueParameter)? + .parse::() + } + } + + impl AxisswapOrdering { + const AXIS_COUNT: usize = 3; + + pub fn apply_ordering(&self, input: [f64; Self::AXIS_COUNT]) -> [f64; Self::AXIS_COUNT] { + let mut output = [0.; 3]; + + (0..Self::AXIS_COUNT).for_each(|input_index| { + let (final_location, flip) = self + .0 + .iter() + .enumerate() + .find_map(|(final_location, (axis_number, flip))| { + (axis_number == &(input_index as u8)).then_some((final_location, flip)) + }) + .expect("no axis number in order array"); + + output[final_location] = { + let mut value = input[input_index]; + + if flip == &Flip(true) { + value *= -1.0; + } + + value + }; + }); + + output + } + } + + impl FromStr for AxisswapOrdering { + type Err = ProjError; + + fn from_str(ordering_str: &str) -> Result { + let mut found_axes: [Option<(u8, Flip)>; 3] = [None; Self::AXIS_COUNT]; + + for (found_axis_index, value_str) in ordering_str.split(',').enumerate() { + let value = value_str.parse::().map_err(|_| { + ProjError::InvalidParameterValue( + "unable to parse comma separated value into integer", + ) + })?; + + let (axis_number, flip) = match value < 0 { + true => (-value as u8, Flip(true)), + false => (value as u8, Flip(false)), + }; + + if axis_number == 0 { + return Err(ProjError::InvalidParameterValue( + "axis value out of range: 0", + )); + } + + if axis_number > Self::AXIS_COUNT as u8 { + return Err(ProjError::InvalidParameterValue( + "axis value larger than number of dimensions", + )); + } + + found_axes[found_axis_index] = Some((axis_number, flip)); + } + + let mut to_swap: [Option<(u8, Flip)>; 3] = [None; Self::AXIS_COUNT]; + + // fill unspecifed values in to_swap with no_op + for maybe_unspecified_axis in 1..(Self::AXIS_COUNT + 1) { + if !found_axes.iter().any(|element| { + element + .is_some_and(|(found_axis, _)| found_axis == maybe_unspecified_axis as u8) + }) { + let unspecified_axis = maybe_unspecified_axis; + to_swap[unspecified_axis - 1] = Some((unspecified_axis as u8, Flip(false))); + } + } + + // fill found axis in whichever locations are not already occupied + for (found_axis, flip) in found_axes.into_iter().flatten() { + let Some(unoccupied_value) = to_swap.iter_mut().find(|element| element.is_none()) + else { + return Err(ProjError::InvalidParameterValue( + "duplicate axes are disallowed", + )); + }; + + *unoccupied_value = Some((found_axis, flip)); + } + + let mut ordering = [(0, Flip(false)); Self::AXIS_COUNT]; + + to_swap + .into_iter() + // all positions should now be filled + .map(|element| element.expect("to swap location not specified")) + .enumerate() + // decrement by one to represent index locations + .for_each(|(index, (to_swap_axis, flip))| { + ordering[index] = (to_swap_axis - 1, flip) + }); + + Ok(Self(ordering)) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + impl AxisswapOrdering { + fn mock(order: [u8; Self::AXIS_COUNT]) -> Self { + Self([ + (order[0], Flip(false)), + (order[1], Flip(false)), + (order[2], Flip(false)), + ]) + } + } + + #[test] + fn performs_order_swap() { + // All possible permutations (3! = 6) + assert_eq!( + AxisswapOrdering::mock([0, 1, 2]).apply_ordering([1., 2., 3.]), + [1., 2., 3.] + ); + + assert_eq!( + AxisswapOrdering::mock([0, 2, 1]).apply_ordering([1., 2., 3.]), + [1., 3., 2.] + ); + + assert_eq!( + AxisswapOrdering::mock([1, 0, 2]).apply_ordering([1., 2., 3.]), + [2., 1., 3.] + ); + + assert_eq!( + AxisswapOrdering::mock([1, 2, 0]).apply_ordering([1., 2., 3.]), + [2., 3., 1.] + ); + + assert_eq!( + AxisswapOrdering::mock([2, 0, 1]).apply_ordering([1., 2., 3.]), + [3., 1., 2.] + ); + + assert_eq!( + AxisswapOrdering::mock([2, 1, 0]).apply_ordering([1., 2., 3.]), + [3., 2., 1.] + ); + } + + #[test] + fn performs_axis_flip() { + assert_eq!( + AxisswapOrdering([(0, Flip(true)), (1, Flip(true)), (2, Flip(false))]) + .apply_ordering([1., -2., 3.]), + [-1., 2., 3.] + ); + } + + #[test] + fn parses_valid_order() { + assert_eq!( + AxisswapOrdering::mock([2, 0, 1]), + "3,1,2".parse::().unwrap() + ) + } + + #[test] + fn parses_only_necessary_pair() { + assert_eq!( + AxisswapOrdering::mock([1, 0, 2]), + "2,1".parse::().unwrap() + ); + assert_eq!( + AxisswapOrdering::mock([2, 1, 0]), + "3,1".parse::().unwrap() + ); + assert_eq!( + AxisswapOrdering::mock([0, 2, 1]), + "3,2".parse::().unwrap() + ); + } + + #[test] + fn parses_singular_value() { + assert_eq!( + AxisswapOrdering::mock([0, 1, 2]), + "3".parse::().unwrap() + ) + } + + #[test] + fn parses_direction() { + assert_eq!( + AxisswapOrdering([(0, Flip(true)), (1, Flip(false)), (2, Flip(true))]), + "-1,2,-3".parse::().unwrap() + ) + } + + #[test] + fn disallows_axis_zero() { + assert!("1,0,2".parse::().is_err()) + } + + #[test] + fn disallows_axis_greater_than_total_count() { + assert!("1,5,3".parse::().is_err()) + } + + #[test] + fn disallows_duplicates() { + assert!("1,2,1".parse::().is_err()) + } + } +} diff --git a/src/conversions/conversion.rs b/src/conversions/conversion.rs new file mode 100644 index 0000000..abded49 --- /dev/null +++ b/src/conversions/conversion.rs @@ -0,0 +1,53 @@ +use crate::*; + +#[derive(Debug)] +pub enum Conversion { + Axisswap(AxisswapConversion), + Noop(NoopConversion), +} + +impl Conversion { + pub fn from_proj_string(proj_str: &str) -> ProjResult { + let parameter_list = projstring::parse(proj_str)?; + + let conversion_name = parameter_list + .get("proj") + .and_then(|parameter| parameter.value) + .ok_or(ProjError::MissingProjectionError)?; + + match conversion_name { + AxisswapConversion::NAME => { + AxisswapConversion::from_params_list(¶meter_list).map(Self::Axisswap) + } + NoopConversion::NAME => { + NoopConversion::from_params_list(¶meter_list).map(Self::Noop) + } + _ => Err(ProjError::InvalidParameterValue("unrecognized projection")), + } + } + + pub fn convert(&self, points: &mut T) -> ProjResult<()> { + match self { + Conversion::Axisswap(conversion) => { + points.transform_coordinates(&mut |x, y, z| conversion.convert(x, y, z)) + } + Conversion::Noop(conversion) => { + points.transform_coordinates(&mut |x, y, z| conversion.convert(x, y, z)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn converts_from_proj_str() { + let mut points = (1.0, 2.0); + Conversion::from_proj_string("+proj=noop") + .unwrap() + .convert(&mut points) + .unwrap(); + } +} diff --git a/src/conversions/convert.rs b/src/conversions/convert.rs new file mode 100644 index 0000000..b5e09c8 --- /dev/null +++ b/src/conversions/convert.rs @@ -0,0 +1,25 @@ +use crate::*; + +pub trait Convert: Sized { + const NAME: &'static str; + + type Parameters: ConvertParameters; + + fn new(parameters: Self::Parameters) -> ProjResult; + + fn convert(&self, x: f64, y: f64, z: f64) -> ProjResult<(f64, f64, f64)>; + + fn from_params_list(parameter_list: &ParamList) -> ProjResult { + Self::new(::from_parameter_list(parameter_list)?) + } +} + +pub trait ConvertParameters: Sized { + fn from_parameter_list(parameter_list: &ParamList) -> ProjResult; +} + +impl ConvertParameters for () { + fn from_parameter_list(_: &ParamList) -> ProjResult { + Ok(()) + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs new file mode 100644 index 0000000..23b4889 --- /dev/null +++ b/src/conversions/mod.rs @@ -0,0 +1,11 @@ +mod conversion; +pub use conversion::Conversion; + +mod convert; +pub(crate) use convert::{Convert, ConvertParameters}; + +mod axisswap; +pub use axisswap::{AxisswapConversion, AxisswapOrdering}; + +mod noop; +pub use noop::NoopConversion; diff --git a/src/conversions/noop.rs b/src/conversions/noop.rs new file mode 100644 index 0000000..4ed0721 --- /dev/null +++ b/src/conversions/noop.rs @@ -0,0 +1,19 @@ +//! Reference +use crate::*; + +#[derive(Debug)] +pub struct NoopConversion; + +impl Convert for NoopConversion { + const NAME: &'static str = "noop"; + + type Parameters = (); + + fn new(_: Self::Parameters) -> ProjResult { + Ok(Self) + } + + fn convert(&self, x: f64, y: f64, z: f64) -> ProjResult<(f64, f64, f64)> { + Ok((x, y, z)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 432c316..1363e30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,17 +93,25 @@ mod ellps; mod geocent; mod math; mod parameters; +pub(crate) use parameters::ParamList; mod parse; mod prime_meridians; mod projstring; mod units; +pub mod conversions; +pub(crate) use conversions::*; + pub mod adaptors; pub mod errors; +pub(crate) use errors::Error as ProjError; +pub(crate) use errors::Result as ProjResult; + pub mod nadgrids; pub mod proj; pub mod projections; pub mod transform; +pub(crate) use transform::Transform; // Reexport pub use proj::Proj; diff --git a/src/transform.rs b/src/transform.rs index a70eeb6..fe21f66 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -100,6 +100,7 @@ where Ok(()) } + // --------------------------------- // Datum transformation // --------------------------------- From 0e508eb1ae15501f93e94f934df95a9c91913334 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 12 Dec 2023 15:16:27 +0100 Subject: [PATCH 2/2] Minor conversion documentation with usage example. --- src/conversions/axisswap.rs | 2 +- src/conversions/mod.rs | 14 ++++++++++++++ src/lib.rs | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/conversions/axisswap.rs b/src/conversions/axisswap.rs index 6a59417..f808c9d 100644 --- a/src/conversions/axisswap.rs +++ b/src/conversions/axisswap.rs @@ -1,4 +1,4 @@ -//! Refernce: https://proj.org/en/9.3/operations/conversions/axisswap.html +//! Refernce: use crate::*; diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 23b4889..ddb0d82 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,3 +1,17 @@ +//! Implemented converions +//! +//! Reference: +//! +//! Example: +//! ``` +//! use proj4rs::conversions::Conversion; +//! +//! let conversion = Conversion::from_proj_string("+proj=axisswap +order=2,1").unwrap(); +//! let mut points = (1., 2., 0.); +//! conversion.convert(&mut points).unwrap(); +//! assert_eq!((2., 1., 0.), points); +//! ``` + mod conversion; pub use conversion::Conversion; diff --git a/src/lib.rs b/src/lib.rs index 1363e30..5588cc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ //! //! Note that angular units are in radians, not degrees ! //! -//! Radian is natural unit for trigonometric opĂ©rations, like proj, proj4rs use radians +//! Radian is natural unit for trigonometric operations, like proj, proj4rs use radians //! for its operation while degrees are mostly used as end user input/output. //! //! Example: