1use crate::CartesianPoint;
2use std::f64::consts::{FRAC_PI_2, PI};
3use std::ops::{Add, Mul, Neg, Sub};
4use wasm_bindgen::prelude::wasm_bindgen;
5
6#[wasm_bindgen]
8#[derive(Debug, Default, Clone, Copy, PartialEq)]
9pub struct GeographicPoint {
10 longitude: f64,
11 latitude: f64,
12 altitude: f64,
13}
14
15impl From<CartesianPoint> for GeographicPoint {
16 fn from(value: CartesianPoint) -> Self {
17 GeographicPoint::from_cartesian(&value)
18 }
19}
20
21#[wasm_bindgen]
22impl GeographicPoint {
23 pub fn new(longitude: f64, latitude: f64, altitude: f64) -> Self {
24 Self {
25 longitude,
26 latitude,
27 altitude,
28 }
29 }
30
31 pub fn from_cartesian(point: &CartesianPoint) -> Self {
33 let radius = f64::sqrt(point.x().powi(2) + point.y().powi(2) + point.z().powi(2));
35
36 let theta = match (point.x(), point.y(), point.z()) {
37 (x, y, z) if z > 0. => f64::atan(f64::sqrt(x.powi(2) + y.powi(2)) / z),
38 (x, y, z) if z < 0. => PI + f64::atan(f64::sqrt(x.powi(2) + y.powi(2)) / z),
39 (x, y, z) if z == 0. && x * y != 0. => FRAC_PI_2,
40 (x, y, z) if x == y && y == z => FRAC_PI_2, _ => FRAC_PI_2, };
44
45 let phi = match (point.x(), point.y()) {
46 (x, y) if x > 0. => f64::atan(y / x),
47 (x, y) if x < 0. && y >= 0. => f64::atan(y / x) + PI,
48 (x, y) if x < 0. && y < 0. => f64::atan(y / x) - PI,
49 (x, y) if x == 0. && y > 0. => FRAC_PI_2,
50 (x, y) if x == 0. && y < 0. => -FRAC_PI_2,
51 (x, y) if x == 0. && y == 0. => 0., _ => 0., };
55
56 Self::default()
57 .with_longitude(phi)
58 .with_latitude(FRAC_PI_2 - theta)
59 .with_altitude(radius)
60 }
61
62 pub fn with_longitude(mut self, value: f64) -> Self {
64 self.set_longitude(value);
65 self
66 }
67
68 pub fn with_latitude(mut self, value: f64) -> Self {
70 self.set_latitude(value);
71 self
72 }
73
74 pub fn with_altitude(mut self, value: f64) -> Self {
76 self.set_altitude(value);
77 self
78 }
79
80 pub fn set_longitude(&mut self, value: f64) {
105 self.longitude = (-PI..=PI)
106 .contains(&value)
107 .then_some(value)
108 .unwrap_or_else(|| {
109 value.add(PI).rem_euclid(2_f64.mul(PI)).sub(PI)
113 })
114 }
115
116 pub fn set_latitude(&mut self, value: f64) {
156 self.latitude = (-FRAC_PI_2..=FRAC_PI_2)
157 .contains(&value)
158 .then_some(value)
159 .unwrap_or_else(|| {
160 if value.cos().is_sign_negative() {
163 let direction = self.longitude.cos().signum().neg(); let rotation = PI * direction;
168 self.set_longitude(self.longitude + rotation);
169 }
170
171 value.sin().asin()
172 });
173 }
174
175 pub fn set_altitude(&mut self, value: f64) {
177 self.altitude = value;
178 }
179
180 pub fn longitude(&self) -> f64 {
182 self.longitude
183 }
184
185 pub fn latitude(&self) -> f64 {
187 self.latitude
188 }
189
190 pub fn altitude(&self) -> f64 {
192 self.altitude
193 }
194
195 pub fn long_ratio(&self) -> f64 {
198 self.longitude / PI
199 }
200
201 pub fn lat_ratio(&self) -> f64 {
204 self.latitude / FRAC_PI_2
205 }
206
207 pub fn distance(&self, other: &GeographicPoint) -> f64 {
209 (self.latitude().sin() * other.latitude().sin()
210 + self.latitude().cos()
211 * other.latitude().cos()
212 * (self.longitude() - other.longitude()).abs().cos())
213 .acos()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use float_cmp::approx_eq;
221
222 const ULPS: i64 = 2;
223
224 #[test]
225 fn longitude_must_not_exceed_boundaries() {
226 struct TestCase {
227 name: &'static str,
228 input: f64,
229 longitude: f64,
230 }
231
232 vec![
233 TestCase {
234 name: "positive longitude value must not change",
235 input: 1.,
236 longitude: 1.,
237 },
238 TestCase {
239 name: "negative longitude value must not change",
240 input: -3.,
241 longitude: -3.,
242 },
243 TestCase {
244 name: "positive overflowing longitude must change",
245 input: PI + 1.,
246 longitude: -PI + 1.,
247 },
248 TestCase {
249 name: "negative overflowing longitude must change",
250 input: -PI - 1.,
251 longitude: PI - 1.,
252 },
253 ]
254 .into_iter()
255 .for_each(|test_case| {
256 let point = GeographicPoint::default().with_longitude(test_case.input);
257 assert!(
258 approx_eq!(f64, point.longitude, test_case.longitude, ulps = ULPS),
259 "{}: {} ±ε = {}",
260 test_case.name,
261 point.longitude,
262 test_case.longitude
263 );
264 });
265 }
266
267 #[test]
268 fn latitude_must_not_exceed_boundaries() {
269 struct TestCase {
270 name: &'static str,
271 input: f64,
272 latitude: f64,
273 }
274
275 vec![
276 TestCase {
277 name: "positive latitude value must not change",
278 input: 1.,
279 latitude: 1.,
280 },
281 TestCase {
282 name: "negative latitude value must not change",
283 input: -1.,
284 latitude: -1.,
285 },
286 TestCase {
287 name: "positive overflowing latitude must change",
288 input: 7. * PI / 4.,
289 latitude: -PI / 4.,
290 },
291 TestCase {
292 name: "negative overflowing latidude must change",
293 input: -7. * PI / 4.,
294 latitude: PI / 4.,
295 },
296 ]
297 .into_iter()
298 .for_each(|test_case| {
299 let point = GeographicPoint::default().with_latitude(test_case.input);
300 assert!(
301 approx_eq!(f64, point.latitude, test_case.latitude, ulps = ULPS),
302 "{}: {} ±ε = {}",
303 test_case.name,
304 point.latitude,
305 test_case.latitude
306 );
307 });
308 }
309
310 #[test]
311 fn longitude_may_change_based_on_latitude() {
312 struct TestCase {
313 name: &'static str,
314 input: f64,
315 longitude: f64,
316 }
317
318 vec![
319 TestCase {
320 name: "positive latitude value must not change longitude",
321 input: 1.,
322 longitude: 0.,
323 },
324 TestCase {
325 name: "negative latitude value must not change longitude",
326 input: -1.,
327 longitude: 0.,
328 },
329 TestCase {
330 name: "positive overflowing latitude must not change longitude",
331 input: 7. * PI / 4.,
332 longitude: 0.,
333 },
334 TestCase {
335 name: "negative overflowing latidude must not change longitude",
336 input: -7. * PI / 4.,
337 longitude: 0.,
338 },
339 TestCase {
340 name: "positive overflowing latitude must change longitude",
341 input: 5. * PI / 4.,
342 longitude: -PI,
343 },
344 TestCase {
345 name: "negative overflowing latidude must change longitude",
346 input: -PI,
347 longitude: -PI,
348 },
349 ]
350 .into_iter()
351 .for_each(|test_case| {
352 let point = GeographicPoint::default().with_latitude(test_case.input);
353 assert!(
354 approx_eq!(f64, point.longitude, test_case.longitude, ulps = ULPS),
355 "{}: {} ±ε = {}",
356 test_case.name,
357 point.longitude,
358 test_case.longitude
359 );
360 });
361 }
362
363 #[test]
364 fn long_ratio_must_not_exceed_boundaries() {
365 struct TestCase {
366 name: &'static str,
367 longitude: f64,
368 ratio: f64,
369 }
370
371 vec![
372 TestCase {
373 name: "zero longitude must be equals to zero",
374 longitude: 0.,
375 ratio: 0.,
376 },
377 TestCase {
378 name: "positive longitude boundary must equals to positive one",
379 longitude: PI,
380 ratio: 1.,
381 },
382 TestCase {
383 name: "arbitrary positive longitude must be positive",
384 longitude: FRAC_PI_2,
385 ratio: 0.5,
386 },
387 TestCase {
388 name: "negative longitude boundary must equals to negative one",
389 longitude: -PI,
390 ratio: -1.,
391 },
392 TestCase {
393 name: "arbitrary negative longitude must be negative",
394 longitude: -FRAC_PI_2,
395 ratio: -0.5,
396 },
397 ]
398 .into_iter()
399 .for_each(|test_case| {
400 let point = GeographicPoint::default().with_longitude(test_case.longitude);
401 assert!(
402 approx_eq!(f64, point.long_ratio(), test_case.ratio, ulps = ULPS),
403 "{}: {} ±ε = {}",
404 test_case.name,
405 point.long_ratio(),
406 test_case.ratio
407 );
408 });
409 }
410
411 #[test]
412 fn lat_ratio_must_not_exceed_boundaries() {
413 struct TestCase {
414 name: &'static str,
415 latitude: f64,
416 ratio: f64,
417 }
418
419 vec![
420 TestCase {
421 name: "zero latitude must be equals to zero",
422 latitude: 0.,
423 ratio: 0.,
424 },
425 TestCase {
426 name: "positive latitude boundary must equals to one",
427 latitude: FRAC_PI_2,
428 ratio: 1.,
429 },
430 TestCase {
431 name: "arbitrary positive latitude must be positive",
432 latitude: FRAC_PI_2 / 2.,
433 ratio: 0.5,
434 },
435 TestCase {
436 name: "negative latitude boundary must equals to negative one",
437 latitude: -FRAC_PI_2,
438 ratio: -1.,
439 },
440 TestCase {
441 name: "arbitrary negative latitude must be negative",
442 latitude: -FRAC_PI_2 / 2.,
443 ratio: -0.5,
444 },
445 ]
446 .into_iter()
447 .for_each(|test_case| {
448 let point = GeographicPoint::default().with_latitude(test_case.latitude);
449 assert!(
450 approx_eq!(f64, point.lat_ratio(), test_case.ratio, ulps = ULPS),
451 "{}: {} ±ε = {}",
452 test_case.name,
453 point.lat_ratio(),
454 test_case.ratio
455 );
456 });
457 }
458
459 #[test]
460 fn basic_operations_must_be_consistent() {
461 let mut point = GeographicPoint::default()
462 .with_longitude(-FRAC_PI_2)
463 .with_latitude(FRAC_PI_2 / 2.);
464
465 point.set_latitude(point.latitude().add(PI));
466
467 assert!(
468 approx_eq!(f64, point.longitude(), FRAC_PI_2, ulps = ULPS),
469 "longitude must switch to positive: {} ±ε = {}",
470 point.longitude(),
471 FRAC_PI_2
472 );
473
474 assert!(
475 approx_eq!(f64, point.latitude(), -FRAC_PI_2 / 2., ulps = ULPS),
476 "latitude must switch to negative: {} ±ε = {}",
477 point.latitude(),
478 -FRAC_PI_2 / 2.
479 );
480 }
481
482 #[test]
483 fn geographic_from_cartesian_must_not_fail() {
484 struct TestCase {
485 name: &'static str,
486 geographic: GeographicPoint,
487 cartesian: CartesianPoint,
488 }
489
490 vec![
491 TestCase {
492 name: "north point",
493 geographic: GeographicPoint::default()
494 .with_latitude(FRAC_PI_2)
495 .with_altitude(1.),
496 cartesian: CartesianPoint::new(0., 0., 1.),
497 },
498 TestCase {
499 name: "south point",
500 geographic: GeographicPoint::default()
501 .with_latitude(-FRAC_PI_2)
502 .with_altitude(1.),
503 cartesian: CartesianPoint::new(0., 0., -1.),
504 },
505 TestCase {
506 name: "east point",
507 geographic: GeographicPoint::default()
508 .with_longitude(FRAC_PI_2)
509 .with_altitude(1.),
510 cartesian: CartesianPoint::new(0., 1., 0.),
511 },
512 TestCase {
513 name: "weast point",
514 geographic: GeographicPoint::default()
515 .with_longitude(-FRAC_PI_2)
516 .with_altitude(1.),
517 cartesian: CartesianPoint::new(0., -1., 0.),
518 },
519 TestCase {
520 name: "front point",
521 geographic: GeographicPoint::default().with_altitude(1.),
522 cartesian: CartesianPoint::new(1., 0., 0.),
523 },
524 TestCase {
525 name: "back point",
526 geographic: GeographicPoint::default()
527 .with_longitude(PI)
528 .with_altitude(1.),
529 cartesian: CartesianPoint::new(-1., 0., 0.),
530 },
531 ]
532 .into_iter()
533 .for_each(|test_case| {
534 let point = GeographicPoint::from(test_case.cartesian);
535 assert!(
536 approx_eq!(
537 f64,
538 point.longitude(),
539 test_case.geographic.longitude(),
540 ulps = ULPS
541 ),
542 "{}: longitude {:#?} ±ε == {:#?}",
543 test_case.name,
544 point.longitude(),
545 test_case.geographic.longitude(),
546 );
547
548 assert!(
549 approx_eq!(
550 f64,
551 point.latitude(),
552 test_case.geographic.latitude(),
553 ulps = ULPS
554 ),
555 "{}: latitude {:#?} ±ε == {:#?}",
556 test_case.name,
557 point.latitude(),
558 test_case.geographic.latitude(),
559 );
560
561 assert!(
562 approx_eq!(
563 f64,
564 point.altitude(),
565 test_case.geographic.altitude(),
566 ulps = ULPS
567 ),
568 "{}: altitude {:#?} ±ε == {:#?}",
569 test_case.name,
570 point.altitude(),
571 test_case.geographic.altitude(),
572 );
573 });
574 }
575
576 #[test]
577 fn distance_must_not_fail() {
578 struct TestCase<'a> {
579 name: &'a str,
580 from: GeographicPoint,
581 to: GeographicPoint,
582 distance: f64,
583 }
584
585 vec![
586 TestCase {
587 name: "Same point must be zero",
588 from: GeographicPoint::default(),
589 to: GeographicPoint::default(),
590 distance: 0.,
591 },
592 TestCase {
593 name: "Oposite points in the horizontal",
594 from: GeographicPoint::default(),
595 to: GeographicPoint::default().with_longitude(-PI),
596 distance: PI,
597 },
598 TestCase {
599 name: "Oposite points in the vertical",
600 from: GeographicPoint::default().with_latitude(FRAC_PI_2),
601 to: GeographicPoint::default().with_latitude(-FRAC_PI_2),
602 distance: PI,
603 },
604 ]
605 .into_iter()
606 .for_each(|test_case| {
607 let got = test_case.from.distance(&test_case.to);
608
609 assert!(
610 approx_eq!(f64, got, test_case.distance, ulps = ULPS),
611 "{}: distance {:#?} ±ε == {:#?}",
612 test_case.name,
613 got,
614 test_case.distance,
615 )
616 });
617 }
618}