globe_rs/
cartesian.rs

1use crate::GeographicPoint;
2use nalgebra::{Matrix3, Vector3};
3use std::{
4    f64::consts::{FRAC_PI_2, PI},
5    ops::{Div, Index, IndexMut, Sub},
6};
7use wasm_bindgen::prelude::wasm_bindgen;
8
9/// Represents a point using the Cartesian system of coordinates.
10#[wasm_bindgen]
11#[derive(Debug, Default, Clone, Copy, PartialEq)]
12pub struct CartesianPoint(Vector3<f64>);
13
14impl From<GeographicPoint> for CartesianPoint {
15    fn from(value: GeographicPoint) -> Self {
16        CartesianPoint::from_geographic(&value)
17    }
18}
19
20impl From<Vector3<f64>> for CartesianPoint {
21    fn from(value: Vector3<f64>) -> Self {
22        CartesianPoint(value)
23    }
24}
25
26impl Index<usize> for CartesianPoint {
27    type Output = f64;
28
29    fn index(&self, index: usize) -> &<Self as Index<usize>>::Output {
30        &self.0[index]
31    }
32}
33
34impl IndexMut<usize> for CartesianPoint {
35    fn index_mut(&mut self, index: usize) -> &mut <Self as Index<usize>>::Output {
36        &mut self.0[index]
37    }
38}
39
40#[wasm_bindgen]
41impl CartesianPoint {
42    #[wasm_bindgen(constructor)]
43    pub fn new(x: f64, y: f64, z: f64) -> Self {
44        Self(Vector3::new(x, y, z))
45    }
46
47    /// Returns the equivalent [`CartesianPoint`] of the given [`GeographicPoint`]
48    pub fn from_geographic(point: &GeographicPoint) -> Self {
49        let radial_distance = match point.altitude() {
50            altitude if altitude == 0. => 1.,
51            altitude => altitude,
52        };
53
54        let theta = FRAC_PI_2 - point.latitude();
55        let phi = point.longitude();
56
57        // improve sin & cos precision for exact numbers
58        let precise_sin_cos = |rad: f64| -> (f64, f64) {
59            if rad.abs() == FRAC_PI_2 {
60                return (rad.signum(), 0.);
61            } else if rad.abs() == PI {
62                return (0., -1.);
63            } else if rad == 0. {
64                return (0., 1.);
65            }
66
67            (rad.sin(), rad.cos())
68        };
69
70        let (theta_sin, theta_cos) = precise_sin_cos(theta);
71        let (phi_sin, phi_cos) = precise_sin_cos(phi);
72
73        Self(Vector3::new(
74            radial_distance * theta_sin * phi_cos,
75            radial_distance * theta_sin * phi_sin,
76            radial_distance * theta_cos,
77        ))
78    }
79
80    #[inline(always)]
81    pub fn x(&self) -> f64 {
82        self[0]
83    }
84
85    #[inline(always)]
86    pub fn set_x(&mut self, x: f64) {
87        self[0] = x;
88    }
89
90    #[inline(always)]
91    pub fn y(&self) -> f64 {
92        self[1]
93    }
94
95    #[inline(always)]
96    pub fn set_y(&mut self, y: f64) {
97        self[1] = y;
98    }
99
100    #[inline(always)]
101    pub fn z(&self) -> f64 {
102        self[2]
103    }
104
105    #[inline(always)]
106    pub fn set_z(&mut self, z: f64) {
107        self[2] = z;
108    }
109
110    /// Returns the distance between self and the given point.
111    pub fn distance(&self, other: &CartesianPoint) -> f64 {
112        (self.x().sub(other.x()).powi(2)
113            + self.y().sub(other.y()).powi(2)
114            + self.z().sub(other.z()).powi(2))
115        .sqrt()
116    }
117
118    /// Performs the cartesian product between self and the given point.
119    pub fn cross(&self, other: &CartesianPoint) -> Self {
120        self.0.cross(&other.0).into()
121    }
122
123    /// Rotates self in theta radians about the edge passing by the origin and the given axis point.
124    pub fn rotate(&mut self, axis: Self, theta: f64) {
125        if self.0.normalize() == axis.0.normalize() {
126            // the point belongs to the axis line, so the rotation takes no effect
127            return;
128        }
129
130        let d = (axis.y().powi(2) + axis.z().powi(2)).sqrt();
131        let cd = axis.z() * d;
132
133        let rz = Matrix3::new(
134            theta.cos(),
135            theta.sin(),
136            0.,
137            -theta.sin(),
138            theta.cos(),
139            0.,
140            0.,
141            0.,
142            1.,
143        );
144
145        let ry = Matrix3::new(d, 0., -axis.x(), 0., 1., 0., axis.x(), 0., d);
146        let ry_inv = Matrix3::new(d, 0., axis.x(), 0., 1., 0., -axis.x(), 0., d);
147
148        if d == 0. {
149            // the rotation axis is already perpendicular to the xy plane.
150            self.0 = ry_inv * rz * ry * self.0;
151            return;
152        }
153
154        let c_div_d = Vector3::new(0., 0., axis.z())
155            .dot(&Vector3::new(0., axis.y(), axis.z()))
156            .div(cd);
157
158        let b_div_d = Vector3::new(0., 0., axis.z())
159            .cross(&Vector3::new(0., axis.y(), axis.z()))
160            .norm()
161            .div(cd);
162
163        let rx = Matrix3::new(1., 0., 0., 0., c_div_d, -b_div_d, 0., b_div_d, c_div_d);
164        let rx_inv = Matrix3::new(1., 0., 0., 0., c_div_d, b_div_d, 0., -b_div_d, c_div_d);
165
166        self.0 = rx_inv * ry_inv * rz * ry * rx * self.0;
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use float_cmp::approx_eq;
174
175    const ULPS: i64 = 2;
176
177    #[test]
178    fn cartesian_from_geographic_must_not_fail() {
179        struct TestCase {
180            name: &'static str,
181            geographic: GeographicPoint,
182            cartesian: CartesianPoint,
183        }
184
185        vec![
186            TestCase {
187                name: "north point",
188                geographic: GeographicPoint::default().with_latitude(FRAC_PI_2),
189                cartesian: CartesianPoint::new(0., 0., 1.),
190            },
191            TestCase {
192                name: "south point",
193                geographic: GeographicPoint::default().with_latitude(-FRAC_PI_2),
194                cartesian: CartesianPoint::new(0., 0., -1.),
195            },
196            TestCase {
197                name: "east point",
198                geographic: GeographicPoint::default().with_longitude(FRAC_PI_2),
199                cartesian: CartesianPoint::new(0., 1., 0.),
200            },
201            TestCase {
202                name: "weast point",
203                geographic: GeographicPoint::default().with_longitude(-FRAC_PI_2),
204                cartesian: CartesianPoint::new(0., -1., 0.),
205            },
206            TestCase {
207                name: "front point",
208                geographic: GeographicPoint::default(),
209                cartesian: CartesianPoint::new(1., 0., 0.),
210            },
211            TestCase {
212                name: "back point as negative bound",
213                geographic: GeographicPoint::default().with_longitude(-PI),
214                cartesian: CartesianPoint::new(-1., 0., 0.),
215            },
216            TestCase {
217                name: "back point as positive bound",
218                geographic: GeographicPoint::default().with_longitude(PI),
219                cartesian: CartesianPoint::new(-1., 0., 0.),
220            },
221        ]
222        .into_iter()
223        .for_each(|test_case| {
224            let point = CartesianPoint::from(test_case.geographic);
225            assert!(
226                approx_eq!(
227                    &[f64],
228                    point.0.as_slice(),
229                    test_case.cartesian.0.as_slice(),
230                    ulps = ULPS
231                ),
232                "{}: {} ±ε = {}",
233                test_case.name,
234                point.0,
235                test_case.cartesian.0
236            );
237        });
238    }
239
240    #[test]
241    fn rotate_must_not_fail() {
242        const EPSILON: f64 = 0.00000000000000024492935982947064;
243
244        struct TestCase {
245            name: &'static str,
246            theta: f64,
247            axis: CartesianPoint,
248            origin: CartesianPoint,
249            want: CartesianPoint,
250        }
251
252        vec![
253            TestCase {
254                name: "full rotation on the x axis must not change the y point",
255                theta: 2. * PI,
256                axis: CartesianPoint::new(1., 0., 0.),
257                origin: CartesianPoint::new(0., 1., 0.),
258                want: CartesianPoint::new(0., 1., 0.),
259            },
260            TestCase {
261                name: "half of a whole rotation on the x axis must change the y point",
262                theta: PI,
263                axis: CartesianPoint::new(1., 0., 0.),
264                origin: CartesianPoint::new(0., 1., 0.),
265                want: CartesianPoint::new(0., -1., 0.),
266            },
267            TestCase {
268                name: "a quarter of a whole rotation on the x axis must change the y point",
269                theta: FRAC_PI_2,
270                axis: CartesianPoint::new(1., 0., 0.),
271                origin: CartesianPoint::new(0., 1., 0.),
272                want: CartesianPoint::new(0., 0., -1.),
273            },
274            TestCase {
275                name: "full rotation on the z axis must not change the y point",
276                theta: 2. * PI,
277                axis: CartesianPoint::new(0., 0., 1.),
278                origin: CartesianPoint::new(0., 1., 0.),
279                want: CartesianPoint::new(0., 1., 0.),
280            },
281            TestCase {
282                name: "half of a whole rotation on the z axis must change the y point",
283                theta: PI,
284                axis: CartesianPoint::new(0., 0., 1.),
285                origin: CartesianPoint::new(0., 1., 0.),
286                want: CartesianPoint::new(0., -1., 0.),
287            },
288            TestCase {
289                name: "a quarter of a whole rotation on the z axis must change the y point",
290                theta: FRAC_PI_2,
291                axis: CartesianPoint::new(0., 0., 1.),
292                origin: CartesianPoint::new(0., 1., 0.),
293                want: CartesianPoint::new(1., 0., 0.),
294            },
295            TestCase {
296                name: "rotate over itself must not change the point",
297                theta: FRAC_PI_2,
298                axis: CartesianPoint::new(0., 1., 0.),
299                origin: CartesianPoint::new(0., 1., 0.),
300                want: CartesianPoint::new(0., 1., 0.),
301            },
302        ]
303        .into_iter()
304        .for_each(|mut test_case| {
305            test_case.origin.rotate(test_case.axis, test_case.theta);
306
307            assert!(
308                approx_eq!(
309                    &[f64],
310                    test_case.origin.0.as_slice(),
311                    test_case.want.0.as_slice(),
312                    epsilon = EPSILON,
313                    ulps = 17
314                ),
315                "{}: {} ±ε = {}",
316                test_case.name,
317                test_case.origin.0,
318                test_case.want.0
319            );
320        });
321    }
322}