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#[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 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 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 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 pub fn cross(&self, other: &CartesianPoint) -> Self {
120 self.0.cross(&other.0).into()
121 }
122
123 pub fn rotate(&mut self, axis: Self, theta: f64) {
125 if self.0.normalize() == axis.0.normalize() {
126 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 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}