Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial conversions support. #12

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions src/conversions/axisswap.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<Self> {
parameter_list
.get("order")
.ok_or(ProjError::NoValueParameter)?
.value
.ok_or(ProjError::NoValueParameter)?
.parse::<AxisswapOrdering>()
}
}

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<Self, Self::Err> {
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::<i8>().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::<AxisswapOrdering>().unwrap()
)
}

#[test]
fn parses_only_necessary_pair() {
assert_eq!(
AxisswapOrdering::mock([1, 0, 2]),
"2,1".parse::<AxisswapOrdering>().unwrap()
);
assert_eq!(
AxisswapOrdering::mock([2, 1, 0]),
"3,1".parse::<AxisswapOrdering>().unwrap()
);
assert_eq!(
AxisswapOrdering::mock([0, 2, 1]),
"3,2".parse::<AxisswapOrdering>().unwrap()
);
}

#[test]
fn parses_singular_value() {
assert_eq!(
AxisswapOrdering::mock([0, 1, 2]),
"3".parse::<AxisswapOrdering>().unwrap()
)
}

#[test]
fn parses_direction() {
assert_eq!(
AxisswapOrdering([(0, Flip(true)), (1, Flip(false)), (2, Flip(true))]),
"-1,2,-3".parse::<AxisswapOrdering>().unwrap()
)
}

#[test]
fn disallows_axis_zero() {
assert!("1,0,2".parse::<AxisswapOrdering>().is_err())
}

#[test]
fn disallows_axis_greater_than_total_count() {
assert!("1,5,3".parse::<AxisswapOrdering>().is_err())
}

#[test]
fn disallows_duplicates() {
assert!("1,2,1".parse::<AxisswapOrdering>().is_err())
}
}
}
53 changes: 53 additions & 0 deletions src/conversions/conversion.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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(&parameter_list).map(Self::Axisswap)
}
NoopConversion::NAME => {
NoopConversion::from_params_list(&parameter_list).map(Self::Noop)
}
_ => Err(ProjError::InvalidParameterValue("unrecognized projection")),
}
}

pub fn convert<T: Transform>(&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();
}
}
25 changes: 25 additions & 0 deletions src/conversions/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use crate::*;

pub trait Convert: Sized {
const NAME: &'static str;

type Parameters: ConvertParameters;

fn new(parameters: Self::Parameters) -> ProjResult<Self>;

fn convert(&self, x: f64, y: f64, z: f64) -> ProjResult<(f64, f64, f64)>;

fn from_params_list(parameter_list: &ParamList) -> ProjResult<Self> {
Self::new(<Self::Parameters as ConvertParameters>::from_parameter_list(parameter_list)?)
}
}

pub trait ConvertParameters: Sized {
fn from_parameter_list(parameter_list: &ParamList) -> ProjResult<Self>;
}

impl ConvertParameters for () {
fn from_parameter_list(_: &ParamList) -> ProjResult<Self> {
Ok(())
}
}
25 changes: 25 additions & 0 deletions src/conversions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Implemented converions
//!
//! Reference: <https://proj.org/en/9.3/operations/conversions>
//!
//! 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;

mod convert;
pub(crate) use convert::{Convert, ConvertParameters};

mod axisswap;
pub use axisswap::{AxisswapConversion, AxisswapOrdering};

mod noop;
pub use noop::NoopConversion;
Loading