geocart 0.1.2

A bridge between geographic and cartesian coordinates.
Documentation
//! Rotation transformation.

use num_traits::{Float, FloatConst, Signed};

use crate::{cartesian::Cartesian, radian::Radian};

use super::Transform;

/// Implements the [geometric transformation](https://en.wikipedia.org/wiki/Rotation_matrix)
/// through which an arbitrary cartesian point can be rotated given an axis and an angle of
/// rotation.
///
/// ## Statement
/// Being v a vector in ℝ3 and k a unit vector describing an axis of rotation about which v rotates
/// by an angle θ, the rotation transformation rotates v according to the right hand rule.
///
/// ## Example
/// ```
/// use std::f64::consts::FRAC_PI_2;
///
/// use geocart::{
///     Cartesian,
///     transform::{Rotation, Transform},
/// };
///
/// // due precision error both values may not be exactly the same
/// let tolerance = 1e-09;
///
/// let rotated = Rotation::noop()
///     .with_axis(Cartesian::origin().with_x(1.))
///     .with_theta(FRAC_PI_2.into())
///     .transform(Cartesian::origin().with_y(1.));
///
/// rotated
///     .into_iter()
///     .zip(Cartesian::origin().with_z(1.))
///     .for_each(|(got, want)| {
///         assert!(
///             (got - want).abs() < tolerance,
///             "point at y1 should be rotated around the x axis to z1",
///         );
///     });
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Rotation<T> {
    /// The axis of rotation about which perform the transformation.
    pub axis: Cartesian<T>,
    /// The angle of rotation.
    pub theta: Radian<T>,
}

impl<T> Transform<Cartesian<T>> for Rotation<T>
where
    T: Float,
{
    fn transform(&self, coords: Cartesian<T>) -> Cartesian<T> {
        coords * self.theta.into_inner().cos()
            + self.axis.cross(&coords) * self.theta.into_inner().sin()
            + self.axis * self.axis.dot(&coords) * (T::one() - self.theta.into_inner().cos())
    }
}

impl<T> Rotation<T>
where
    T: Signed + Float + FloatConst,
{
    /// Creates a rotation instance that performs no transformation.
    pub fn noop() -> Self {
        Self {
            axis: Cartesian::origin(),
            theta: T::zero().into(),
        }
    }
}

impl<T> Rotation<T> {
    pub fn with_axis(self, axis: Cartesian<T>) -> Self {
        Self { axis, ..self }
    }

    pub fn with_theta(self, theta: Radian<T>) -> Self {
        Self { theta, ..self }
    }
}

#[cfg(test)]
mod tests {
    use std::f64::consts::{FRAC_PI_2, PI};

    use crate::{
        radian::Radian,
        transform::{Rotation, Transform},
        Cartesian,
    };

    #[test]
    fn cartesian_rotation() {
        struct Test {
            name: &'static str,
            theta: Radian<f64>,
            axis: Cartesian<f64>,
            input: Cartesian<f64>,
            output: Cartesian<f64>,
        }

        vec![
            Test {
                name: "noop rotation must not change the point",
                theta: Radian::from(0.),
                axis: Cartesian::origin(),
                input: Cartesian::origin().with_x(1.).with_y(2.).with_z(3.),
                output: Cartesian::origin().with_x(1.).with_y(2.).with_z(3.),
            },
            Test {
                name: "full rotation on the x axis must not change the y point",
                theta: Radian::from(2. * PI),
                axis: Cartesian::origin().with_x(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_y(1.),
            },
            Test {
                name: "half of a whole rotation on the x axis must change the y point",
                theta: Radian::from(PI),
                axis: Cartesian::origin().with_x(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_y(-1.),
            },
            Test {
                name: "a quarter of a whole rotation on the x axis must change the y point",
                theta: Radian::from(FRAC_PI_2),
                axis: Cartesian::origin().with_x(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_z(1.),
            },
            Test {
                name: "full rotation on the z axis must not change the y point",
                theta: Radian::from(2. * PI),
                axis: Cartesian::origin().with_z(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_y(1.),
            },
            Test {
                name: "half of a whole rotation on the z axis must change the y point",
                theta: Radian::from(PI),
                axis: Cartesian::origin().with_z(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_y(-1.),
            },
            Test {
                name: "a quarter of a whole rotation on the z axis must change the y point",
                theta: Radian::from(FRAC_PI_2),
                axis: Cartesian::origin().with_z(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_x(-1.),
            },
            Test {
                name: "rotate over itself must not change the point",
                theta: Radian::from(FRAC_PI_2),
                axis: Cartesian::origin().with_y(1.),
                input: Cartesian::origin().with_y(1.),
                output: Cartesian::origin().with_y(1.),
            },
        ]
        .into_iter()
        .for_each(|test| {
            let rotated = Rotation::noop()
                .with_axis(test.axis)
                .with_theta(test.theta)
                .transform(test.input);

            let tolerance = 1e-09;

            rotated
                .into_iter()
                .zip(test.output)
                .for_each(|(got, want)| {
                    assert!(
                        (got - want).abs() < tolerance,
                        "{}: got rotated = {:?}, want {:?}",
                        test.name,
                        rotated,
                        test.output
                    );
                });
        });
    }
}