ofdb_entities/
geo.rs

1use std::{
2    num::ParseFloatError,
3    ops::{Add, Sub},
4};
5
6use itertools::*;
7use thiserror::Error;
8
9pub type RawCoord = i32;
10
11// Assumption: 2-complement binary representation
12const RAW_COORD_INVALID: RawCoord = std::i32::MIN;
13const RAW_COORD_MAX: RawCoord = std::i32::MAX;
14const RAW_COORD_MIN: RawCoord = -RAW_COORD_MAX;
15
16#[derive(Debug, Error)]
17pub enum CoordRangeError {
18    #[error("degrees out of range")]
19    Degrees(f64),
20
21    #[error("radians out of range")]
22    Radians(f64),
23}
24
25#[derive(Debug, Error)]
26pub enum CoordInputError {
27    #[error(transparent)]
28    Range(#[from] CoordRangeError),
29
30    #[error(transparent)]
31    Parse(#[from] ParseFloatError),
32}
33
34/// Compact fixed-point integer representation of a geographical coordinate.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct GeoCoord(RawCoord);
37
38impl GeoCoord {
39    const INVALID: Self = Self(RAW_COORD_INVALID);
40
41    pub const fn max() -> Self {
42        Self(RAW_COORD_MAX)
43    }
44
45    pub const fn min() -> Self {
46        Self(RAW_COORD_MIN)
47    }
48
49    pub const fn to_raw(self) -> RawCoord {
50        self.0
51    }
52
53    pub const fn from_raw(raw: RawCoord) -> Self {
54        Self(raw)
55    }
56
57    pub fn is_valid(self) -> bool {
58        self != Self::INVALID
59    }
60}
61
62impl Default for GeoCoord {
63    fn default() -> Self {
64        let res = Self::INVALID;
65        debug_assert!(!res.is_valid());
66        res
67    }
68}
69
70impl std::cmp::PartialOrd for GeoCoord {
71    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
72        if self == other {
73            Some(std::cmp::Ordering::Equal)
74        } else if self.is_valid() && other.is_valid() {
75            Some(self.to_raw().cmp(&other.to_raw()))
76        } else {
77            None
78        }
79    }
80}
81
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd)]
83pub struct LatCoord(GeoCoord);
84
85impl LatCoord {
86    const RAD_MAX: f64 = std::f64::consts::FRAC_PI_2;
87    const RAD_MIN: f64 = -std::f64::consts::FRAC_PI_2;
88    const TO_RAD: f64 =
89        (Self::RAD_MAX - Self::RAD_MIN) / (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64);
90    const FROM_RAD: f64 =
91        (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64) / (Self::RAD_MAX - Self::RAD_MIN);
92
93    const DEG_MAX: f64 = 90.0;
94    const DEG_MIN: f64 = -90.0;
95    const TO_DEG: f64 =
96        (Self::DEG_MAX - Self::DEG_MIN) / (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64);
97    const FROM_DEG: f64 =
98        (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64) / (Self::DEG_MAX - Self::DEG_MIN);
99
100    pub const fn max() -> Self {
101        Self(GeoCoord::max())
102    }
103
104    pub const fn min() -> Self {
105        Self(GeoCoord::min())
106    }
107
108    pub const fn to_raw(self) -> RawCoord {
109        self.0.to_raw()
110    }
111
112    pub const fn from_raw(raw: RawCoord) -> Self {
113        Self(GeoCoord::from_raw(raw))
114    }
115
116    pub fn is_valid(self) -> bool {
117        self.0.is_valid()
118    }
119
120    #[allow(clippy::absurd_extreme_comparisons)]
121    pub fn to_rad(self) -> f64 {
122        if self.is_valid() {
123            debug_assert!(self.to_raw() >= RAW_COORD_MIN);
124            debug_assert!(self.to_raw() <= RAW_COORD_MAX);
125            let rad = f64::from(self.to_raw()) * Self::TO_RAD;
126            debug_assert!(rad >= Self::RAD_MIN);
127            debug_assert!(rad <= Self::RAD_MAX);
128            rad
129        } else {
130            std::f64::NAN
131        }
132    }
133
134    #[allow(clippy::absurd_extreme_comparisons)]
135    pub fn to_deg(self) -> f64 {
136        if self.is_valid() {
137            debug_assert!(self.to_raw() >= RAW_COORD_MIN);
138            debug_assert!(self.to_raw() <= RAW_COORD_MAX);
139            let deg = f64::from(self.to_raw()) * Self::TO_DEG;
140            debug_assert!(deg >= Self::DEG_MIN);
141            debug_assert!(deg <= Self::DEG_MAX);
142            deg
143        } else {
144            std::f64::NAN
145        }
146    }
147
148    pub fn from_rad<T: Into<f64>>(rad: T) -> Self {
149        let rad = rad.into();
150        debug_assert!(rad >= Self::RAD_MIN);
151        debug_assert!(rad <= Self::RAD_MAX);
152        let raw = f64::round(rad * Self::FROM_RAD) as RawCoord;
153        let res = Self::from_raw(raw);
154        debug_assert!(res.is_valid());
155        res
156    }
157
158    pub fn from_deg<T: Into<f64>>(deg: T) -> Self {
159        let deg = deg.into();
160        debug_assert!(deg >= Self::DEG_MIN);
161        debug_assert!(deg <= Self::DEG_MAX);
162        let raw = f64::round(deg * Self::FROM_DEG) as RawCoord;
163        let res = Self::from_raw(raw);
164        debug_assert!(res.is_valid());
165        res
166    }
167
168    pub fn try_from_deg<T: Into<f64>>(deg: T) -> Result<Self, CoordRangeError> {
169        let deg = deg.into();
170        if (Self::DEG_MIN..=Self::DEG_MAX).contains(&deg) {
171            Ok(Self::from_deg(deg))
172        } else {
173            Err(CoordRangeError::Degrees(deg))
174        }
175    }
176}
177
178impl Add for LatCoord {
179    type Output = Self;
180
181    fn add(self, rhs: Self) -> Self {
182        let mut deg = self.to_deg() + rhs.to_deg();
183        if deg > Self::DEG_MAX {
184            deg -= Self::DEG_MAX - Self::DEG_MIN;
185        }
186        Self::from_deg(deg)
187    }
188}
189
190impl Sub for LatCoord {
191    type Output = Self;
192
193    fn sub(self, rhs: Self) -> Self {
194        let mut deg = self.to_deg() - rhs.to_deg();
195        if deg < Self::DEG_MIN {
196            deg += Self::DEG_MAX - Self::DEG_MIN;
197        }
198        Self::from_deg(deg)
199    }
200}
201
202impl std::fmt::Display for LatCoord {
203    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
204        write!(f, "{}", self.to_deg())
205    }
206}
207
208#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd)]
209pub struct LngCoord(GeoCoord);
210
211impl LngCoord {
212    const RAD_MAX: f64 = std::f64::consts::PI;
213    const RAD_MIN: f64 = -std::f64::consts::PI;
214    const TO_RAD: f64 =
215        (Self::RAD_MAX - Self::RAD_MIN) / (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64);
216    const FROM_RAD: f64 =
217        (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64) / (Self::RAD_MAX - Self::RAD_MIN);
218
219    const DEG_MAX: f64 = 180.0;
220    const DEG_MIN: f64 = -180.0;
221    const TO_DEG: f64 =
222        (Self::DEG_MAX - Self::DEG_MIN) / (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64);
223    const FROM_DEG: f64 =
224        (RAW_COORD_MAX as f64 - RAW_COORD_MIN as f64) / (Self::DEG_MAX - Self::DEG_MIN);
225
226    pub const fn max() -> Self {
227        Self(GeoCoord::max())
228    }
229
230    pub const fn min() -> Self {
231        Self(GeoCoord::min())
232    }
233
234    pub const fn to_raw(self) -> RawCoord {
235        self.0.to_raw()
236    }
237
238    pub const fn from_raw(raw: RawCoord) -> Self {
239        Self(GeoCoord::from_raw(raw))
240    }
241
242    pub fn is_valid(self) -> bool {
243        self.0.is_valid()
244    }
245
246    #[allow(clippy::absurd_extreme_comparisons)]
247    pub fn to_rad(self) -> f64 {
248        if self.is_valid() {
249            debug_assert!(self.to_raw() >= RAW_COORD_MIN);
250            debug_assert!(self.to_raw() <= RAW_COORD_MAX);
251            let rad = f64::from(self.to_raw()) * Self::TO_RAD;
252            debug_assert!(rad >= Self::RAD_MIN);
253            debug_assert!(rad <= Self::RAD_MAX);
254            rad
255        } else {
256            std::f64::NAN
257        }
258    }
259
260    #[allow(clippy::absurd_extreme_comparisons)]
261    pub fn to_deg(self) -> f64 {
262        if self.is_valid() {
263            debug_assert!(self.to_raw() >= RAW_COORD_MIN);
264            debug_assert!(self.to_raw() <= RAW_COORD_MAX);
265            let deg = f64::from(self.to_raw()) * Self::TO_DEG;
266            debug_assert!(deg >= Self::DEG_MIN);
267            debug_assert!(deg <= Self::DEG_MAX);
268            deg
269        } else {
270            std::f64::NAN
271        }
272    }
273
274    pub fn from_rad<T: Into<f64>>(rad: T) -> Self {
275        let rad = rad.into();
276        debug_assert!(rad >= Self::RAD_MIN);
277        debug_assert!(rad <= Self::RAD_MAX);
278        let raw = f64::round(rad * Self::FROM_RAD) as RawCoord;
279        let res = Self::from_raw(raw);
280        debug_assert!(res.is_valid());
281        res
282    }
283
284    pub fn from_deg<T: Into<f64>>(deg: T) -> Self {
285        let deg = deg.into();
286        debug_assert!(deg >= Self::DEG_MIN);
287        debug_assert!(deg <= Self::DEG_MAX);
288        let raw = f64::round(deg * Self::FROM_DEG) as RawCoord;
289        let res = Self::from_raw(raw);
290        debug_assert!(res.is_valid());
291        res
292    }
293
294    pub fn try_from_deg<T: Into<f64>>(deg: T) -> Result<Self, CoordRangeError> {
295        let deg = deg.into();
296        if (Self::DEG_MIN..=Self::DEG_MAX).contains(&deg) {
297            Ok(Self::from_deg(deg))
298        } else {
299            Err(CoordRangeError::Degrees(deg))
300        }
301    }
302}
303
304impl Add for LngCoord {
305    type Output = Self;
306
307    fn add(self, rhs: Self) -> Self {
308        let mut deg = self.to_deg() + rhs.to_deg();
309        if deg > Self::DEG_MAX {
310            deg -= Self::DEG_MAX - Self::DEG_MIN;
311        }
312        Self::from_deg(deg)
313    }
314}
315
316impl Sub for LngCoord {
317    type Output = Self;
318
319    fn sub(self, rhs: Self) -> Self {
320        let mut deg = self.to_deg() - rhs.to_deg();
321        if deg < Self::DEG_MIN {
322            deg += Self::DEG_MAX - Self::DEG_MIN;
323        }
324        Self::from_deg(deg)
325    }
326}
327
328impl std::fmt::Display for LngCoord {
329    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
330        write!(f, "{}", self.to_deg())
331    }
332}
333
334/// Compact internal representation of a geographical location on a (flat) map.
335#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
336pub struct MapPoint {
337    lat: LatCoord,
338    lng: LngCoord,
339}
340
341impl MapPoint {
342    pub const fn new(lat: LatCoord, lng: LngCoord) -> Self {
343        Self { lat, lng }
344    }
345
346    pub const fn lat(self) -> LatCoord {
347        self.lat
348    }
349
350    pub const fn lng(self) -> LngCoord {
351        self.lng
352    }
353
354    pub fn is_valid(self) -> bool {
355        self.lat.is_valid() && self.lng.is_valid()
356    }
357
358    pub fn to_lat_lng_rad(self) -> (f64, f64) {
359        (self.lat.to_rad(), self.lng.to_rad())
360    }
361
362    pub fn to_lat_lng_deg(self) -> (f64, f64) {
363        (self.lat.to_deg(), self.lng.to_deg())
364    }
365
366    pub fn from_lat_lng_deg<LAT: Into<f64>, LNG: Into<f64>>(lat: LAT, lng: LNG) -> Self {
367        Self::new(LatCoord::from_deg(lat), LngCoord::from_deg(lng))
368    }
369
370    pub fn try_from_lat_lng_deg<LAT: Into<f64>, LNG: Into<f64>>(
371        lat: LAT,
372        lng: LNG,
373    ) -> Result<Self, CoordRangeError> {
374        let lat = LatCoord::try_from_deg(lat)?;
375        let lng = LngCoord::try_from_deg(lng)?;
376        Ok(Self::new(lat, lng))
377    }
378
379    fn parse_lat_lng_deg(lat_deg_str: &str, lng_deg_str: &str) -> Result<Self, MapPointInputError> {
380        let lat_deg = lat_deg_str
381            .parse::<f64>()
382            .map_err(|err| MapPointInputError::Latitude(err.into()))?;
383        let lat = LatCoord::try_from_deg(lat_deg)
384            .map_err(|err| MapPointInputError::Latitude(err.into()))?;
385        debug_assert!(lat.is_valid());
386        let lng_deg = lng_deg_str
387            .parse::<f64>()
388            .map_err(|err| MapPointInputError::Longitude(err.into()))?;
389        let lng = LngCoord::try_from_deg(lng_deg)
390            .map_err(|err| MapPointInputError::Longitude(err.into()))?;
391        debug_assert!(lng.is_valid());
392        Ok(MapPoint::new(lat, lng))
393    }
394}
395
396#[derive(Debug, Error)]
397pub enum MapPointInputError {
398    #[error("latitude: {0}")]
399    Latitude(CoordInputError),
400
401    #[error("longitude: {0}")]
402    Longitude(CoordInputError),
403
404    #[error("invalid format: '{0}'")]
405    Format(String),
406}
407
408impl std::fmt::Display for MapPoint {
409    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
410        write!(f, "{},{}", self.lat, self.lng)
411    }
412}
413
414impl std::str::FromStr for MapPoint {
415    type Err = MapPointInputError;
416
417    fn from_str(s: &str) -> Result<Self, Self::Err> {
418        if let Some((lat_deg_str, lng_deg_str)) = s.split(',').collect_tuple() {
419            MapPoint::parse_lat_lng_deg(lat_deg_str, lng_deg_str)
420        } else {
421            Err(MapPointInputError::Format(s.to_string()))
422        }
423    }
424}
425
426#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
427pub struct Distance(pub f64);
428
429impl Distance {
430    pub const fn infinite() -> Self {
431        Self(std::f64::INFINITY)
432    }
433
434    pub const fn from_meters(meters: f64) -> Self {
435        Self(meters)
436    }
437
438    pub const fn to_meters(self) -> f64 {
439        self.0
440    }
441
442    pub fn is_valid(self) -> bool {
443        self.0 >= 0.0
444    }
445}
446
447// Semi-axes of WGS-84 ellipsoid
448const WGS84_MAJOR_SEMIAXIS: Distance = Distance::from_meters(6_378_137.0);
449const WGS84_MINOR_SEMIAXIS: Distance = Distance::from_meters(6_356_752.3);
450
451// Earth radius at a given latitude, according to the WGS-84 ellipsoid
452fn wgs84_earth_radius(lat: LatCoord) -> Distance {
453    let major_n =
454        WGS84_MAJOR_SEMIAXIS.to_meters() * WGS84_MAJOR_SEMIAXIS.to_meters() * lat.to_rad().cos();
455    let minor_n =
456        WGS84_MINOR_SEMIAXIS.to_meters() * WGS84_MINOR_SEMIAXIS.to_meters() * lat.to_rad().sin();
457    let major_d = WGS84_MAJOR_SEMIAXIS.to_meters() * lat.to_rad().cos();
458    let minor_d = WGS84_MINOR_SEMIAXIS.to_meters() * lat.to_rad().sin();
459    Distance::from_meters(
460        ((major_n * major_n + minor_n * minor_n) / (major_d * major_d + minor_d * minor_d)).sqrt(),
461    )
462}
463
464impl MapPoint {
465    /// Calculate the great-circle distance on the surface
466    /// of the earth using a special case of the Vincenty
467    /// formula for numerical accuracy.
468    /// Reference: <https://en.wikipedia.org/wiki/Great-circle_distance>
469    pub fn distance(p1: MapPoint, p2: MapPoint) -> Option<Distance> {
470        if !p1.is_valid() || !p2.is_valid() {
471            return None;
472        }
473
474        let (lat1_rad, lng1_rad) = p1.to_lat_lng_rad();
475        let (lat2_rad, lng2_rad) = p2.to_lat_lng_rad();
476
477        let (lat1_sin, lat1_cos) = (lat1_rad.sin(), lat1_rad.cos());
478        let (lat2_sin, lat2_cos) = (lat2_rad.sin(), lat2_rad.cos());
479
480        let dlng = (lng1_rad - lng2_rad).abs();
481        let (dlng_sin, dlng_cos) = (dlng.sin(), dlng.cos());
482
483        let nom1 = lat2_cos * dlng_sin;
484        let nom2 = lat1_cos * lat2_sin - lat1_sin * lat2_cos * dlng_cos;
485
486        let nom = (nom1 * nom1 + nom2 * nom2).sqrt();
487        let denom = lat1_sin * lat2_sin + lat1_cos * lat2_cos * dlng_cos;
488
489        let mean_earth_radius_meters = (wgs84_earth_radius(p1.lat()).to_meters()
490            + wgs84_earth_radius(p2.lat()).to_meters())
491            / 2.0;
492        Some(Distance::from_meters(
493            mean_earth_radius_meters * nom.atan2(denom),
494        ))
495    }
496}
497
498#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
499pub struct MapBbox {
500    sw: MapPoint,
501    ne: MapPoint,
502}
503
504impl MapBbox {
505    pub const fn new(sw: MapPoint, ne: MapPoint) -> Self {
506        Self { sw, ne }
507    }
508
509    /// Create a bounding box with a center point and given width/height
510    pub fn centered_around(center: MapPoint, lat_width: Distance, lng_height: Distance) -> Self {
511        let lat = center.lat();
512        let lng = center.lng();
513
514        // Radius of the parallel at given latitude
515        let earth_radius = wgs84_earth_radius(lat).to_meters();
516        let parallel_radius = earth_radius * lat.to_rad().cos();
517
518        let lat_delta = LatCoord::from_rad(lat_width.to_meters() / (2.0 * parallel_radius));
519        let lng_delta = LngCoord::from_rad(lng_height.to_meters() / (2.0 * parallel_radius));
520
521        let sw = MapPoint::new(lat - lat_delta, lng - lng_delta);
522        let ne = MapPoint::new(lat + lat_delta, lng + lng_delta);
523
524        Self::new(sw, ne)
525    }
526
527    pub const fn southwest(&self) -> MapPoint {
528        self.sw
529    }
530
531    pub const fn northeast(&self) -> MapPoint {
532        self.ne
533    }
534
535    pub fn is_valid(&self) -> bool {
536        self.sw.is_valid() && self.ne.is_valid() && self.sw.lat() <= self.ne.lat()
537    }
538
539    pub fn is_empty(&self) -> bool {
540        debug_assert!(self.sw.is_valid());
541        debug_assert!(self.ne.is_valid());
542        self.sw.lat() >= self.ne.lat() || self.sw.lng() == self.ne.lng()
543    }
544
545    pub fn contains_point(&self, pt: MapPoint) -> bool {
546        debug_assert!(self.is_valid());
547        debug_assert!(pt.is_valid());
548        if pt.lat() < self.sw.lat() || pt.lat() > self.ne.lat() {
549            return false;
550        }
551        if self.sw.lng() <= self.ne.lng() {
552            // regular (inclusive)
553            pt.lng() >= self.sw.lng() && pt.lng() <= self.ne.lng()
554        } else {
555            // inverse (exclusive)
556            !(pt.lng() > self.ne.lng() && pt.lng() < self.sw.lng())
557        }
558    }
559}
560
561impl std::fmt::Display for MapBbox {
562    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
563        write!(f, "{},{}", self.sw, self.ne)
564    }
565}
566
567impl std::str::FromStr for MapBbox {
568    type Err = MapBboxInputError;
569
570    fn from_str(s: &str) -> Result<Self, Self::Err> {
571        if let Some((sw_lat_deg_str, sw_lng_deg_str, ne_lat_deg_str, ne_lng_deg_str)) =
572            s.split(',').collect_tuple()
573        {
574            let sw = MapPoint::parse_lat_lng_deg(sw_lat_deg_str, sw_lng_deg_str)
575                .map_err(MapBboxInputError::Southwest)?;
576            let ne = MapPoint::parse_lat_lng_deg(ne_lat_deg_str, ne_lng_deg_str)
577                .map_err(MapBboxInputError::Northeast)?;
578            Ok(MapBbox::new(sw, ne))
579        } else {
580            Err(MapBboxInputError::Format(s.to_string()))
581        }
582    }
583}
584
585#[derive(Debug, Error)]
586pub enum MapBboxInputError {
587    #[error("southwest point: {0}")]
588    Southwest(MapPointInputError),
589
590    #[error("northeast point: {0}")]
591    Northeast(MapPointInputError),
592
593    #[error("invalid format: '{0}'")]
594    Format(String),
595}
596
597#[cfg(test)]
598#[allow(clippy::unreadable_literal, clippy::float_cmp)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn latitude() {
604        assert!(!LatCoord::default().is_valid());
605        assert!(LatCoord::default().to_deg().is_nan());
606        assert_eq!(0.0, LatCoord::from_raw(0).to_deg());
607        assert_eq!(RAW_COORD_MIN, LatCoord::min().to_raw());
608        assert_eq!(RAW_COORD_MAX, LatCoord::max().to_raw());
609        assert_eq!(
610            LatCoord::min(),
611            LatCoord::from_raw(LatCoord::min().to_raw())
612        );
613        assert_eq!(
614            LatCoord::max(),
615            LatCoord::from_raw(LatCoord::max().to_raw())
616        );
617        assert_eq!(
618            LatCoord::min(),
619            LatCoord::from_deg(LatCoord::min().to_deg())
620        );
621        assert_eq!(
622            LatCoord::max(),
623            LatCoord::from_deg(LatCoord::max().to_deg())
624        );
625        assert_eq!(LatCoord::min(), LatCoord::from_deg(-90));
626        assert_eq!(LatCoord::max(), LatCoord::from_deg(90));
627        assert!(LatCoord::try_from_deg(-90.000001).is_err());
628        assert!(LatCoord::try_from_deg(90.000001).is_err());
629    }
630
631    #[test]
632    fn longitude() {
633        assert!(!LngCoord::default().is_valid());
634        assert!(LngCoord::default().to_deg().is_nan());
635        assert_eq!(0.0, LngCoord::from_raw(0).to_deg());
636        assert_eq!(RAW_COORD_MIN, LngCoord::min().to_raw());
637        assert_eq!(RAW_COORD_MAX, LngCoord::max().to_raw());
638        assert!(LngCoord::min().is_valid());
639        assert!(LngCoord::max().is_valid());
640        assert_eq!(
641            LngCoord::min(),
642            LngCoord::from_raw(LngCoord::min().to_raw())
643        );
644        assert_eq!(
645            LngCoord::max(),
646            LngCoord::from_raw(LngCoord::max().to_raw())
647        );
648        assert_eq!(
649            LngCoord::min(),
650            LngCoord::from_deg(LngCoord::min().to_deg())
651        );
652        assert_eq!(
653            LngCoord::max(),
654            LngCoord::from_deg(LngCoord::max().to_deg())
655        );
656        assert_eq!(LngCoord::min(), LngCoord::from_deg(-180));
657        assert_eq!(LngCoord::max(), LngCoord::from_deg(180));
658        assert!(LngCoord::try_from_deg(-180.000001).is_err());
659        assert!(LngCoord::try_from_deg(180.000001).is_err());
660    }
661
662    #[test]
663    fn no_distance() {
664        let p1 = MapPoint::from_lat_lng_deg(0.0, 0.0);
665        assert_eq!(MapPoint::distance(p1, p1).unwrap().to_meters(), 0.0);
666
667        let p2 = MapPoint::from_lat_lng_deg(-25.0, 55.0);
668        assert_eq!(MapPoint::distance(p2, p2).unwrap().to_meters(), 0.0);
669
670        let p1 = MapPoint::from_lat_lng_deg(-15.0, -180.0);
671        let p2 = MapPoint::from_lat_lng_deg(-15.0, 180.0);
672        assert!(MapPoint::distance(p1, p2).unwrap().to_meters() < 0.000001);
673    }
674
675    #[test]
676    fn real_distance() {
677        let stuttgart = MapPoint::from_lat_lng_deg(48.7755, 9.1827);
678        let mannheim = MapPoint::from_lat_lng_deg(49.4836, 8.4630);
679        assert!(MapPoint::distance(stuttgart, mannheim).unwrap() > Distance::from_meters(94_000.0));
680        assert!(MapPoint::distance(stuttgart, mannheim).unwrap() < Distance::from_meters(95_000.0));
681
682        let new_york = MapPoint::from_lat_lng_deg(40.714268, -74.005974);
683        let sidney = MapPoint::from_lat_lng_deg(-33.867138, 151.207108);
684        assert!(
685            MapPoint::distance(new_york, sidney).unwrap() > Distance::from_meters(15_985_000.0)
686        );
687        assert!(
688            MapPoint::distance(new_york, sidney).unwrap() < Distance::from_meters(15_995_000.0)
689        );
690    }
691
692    #[test]
693    fn symetric_distance() {
694        let a = MapPoint::from_lat_lng_deg(80.0, 0.0);
695        let b = MapPoint::from_lat_lng_deg(90.0, 20.0);
696        assert_eq!(
697            MapPoint::distance(a, b).unwrap(),
698            MapPoint::distance(b, a).unwrap()
699        );
700    }
701
702    #[test]
703    fn distance_with_invalid_coordinates() {
704        let a = MapPoint::new(LatCoord::from_deg(10.0), Default::default());
705        let b = MapPoint::from_lat_lng_deg(20.0, 20.0);
706        assert_eq!(None, MapPoint::distance(a, b));
707    }
708
709    #[test]
710    fn positive_distance_regressions() {
711        let p1 = MapPoint::from_lat_lng_deg(-81.2281041784343, 77.75747775927069);
712        let p2 = MapPoint::from_lat_lng_deg(40.92116510538438, -93.33303223984923);
713        assert!(MapPoint::distance(p1, p2).unwrap().to_meters() >= 0.0);
714
715        let p1 = MapPoint::from_lat_lng_deg(67.01568147028595, 122.10276824520099);
716        let p2 = MapPoint::from_lat_lng_deg(-87.84709362678561, 132.71691422570353);
717        assert!(MapPoint::distance(p1, p2).unwrap().to_meters() >= 0.0);
718
719        let p1 = MapPoint::from_lat_lng_deg(-37.44489137895633, -124.46758920534867);
720        let p2 = MapPoint::from_lat_lng_deg(29.29724492099939, 0.03218860366949281);
721        assert!(MapPoint::distance(p1, p2).unwrap().to_meters() >= 0.0);
722    }
723
724    #[test]
725    fn bbox_contains_point() {
726        let sw = MapPoint::from_lat_lng_deg(-25.0, -20.0);
727        let ne = MapPoint::from_lat_lng_deg(25.0, 30.0);
728        let bbox = MapBbox::new(sw, ne);
729        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, -15.0)));
730        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-26.0, -15.0)));
731        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, 20.0)));
732        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(26.0, 20.0)));
733        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, -21.0)));
734        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, 31.0)));
735
736        let sw = MapPoint::from_lat_lng_deg(-25.0, 175.0);
737        let ne = MapPoint::from_lat_lng_deg(25.0, -175.0);
738        let bbox = MapBbox::new(sw, ne);
739        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, 177.0)));
740        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-26.0, 177.0)));
741        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, -177.0)));
742        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(26.0, 177.0)));
743        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, 174.0)));
744        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, -174.0)));
745
746        let sw = MapPoint::from_lat_lng_deg(-25.0, 30.0);
747        let ne = MapPoint::from_lat_lng_deg(25.0, 10.0);
748        let bbox = MapBbox::new(sw, ne);
749        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, 5.0)));
750        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-26.0, 5.0)));
751        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, 35.0)));
752        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(26.0, 35.0)));
753        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, 180.0)));
754        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(26.0, 180.0)));
755        assert!(bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, -180.0)));
756        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(26.0, -180.0)));
757        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(-10.0, 11.0)));
758        assert!(!bbox.contains_point(MapPoint::from_lat_lng_deg(10.0, 29.0)));
759
760        let bbox1 = MapBbox::new(
761            MapPoint::from_lat_lng_deg(0.0, 0.0),
762            MapPoint::from_lat_lng_deg(10.0, 10.0),
763        );
764        let bbox2 = MapBbox::new(
765            MapPoint::from_lat_lng_deg(-10.0, 0.0),
766            MapPoint::from_lat_lng_deg(0.0, 10.0),
767        );
768        let bbox3 = MapBbox::new(
769            MapPoint::from_lat_lng_deg(-10.0, -10.0),
770            MapPoint::from_lat_lng_deg(0.0, 0.0),
771        );
772        let bbox4 = MapBbox::new(
773            MapPoint::from_lat_lng_deg(0.0, -10.0),
774            MapPoint::from_lat_lng_deg(10.0, 0.0),
775        );
776
777        let lat1 = 5.0;
778        let lng1 = 5.0;
779        let lat2 = -5.0;
780        let lng2 = 5.0;
781        let lat3 = -5.0;
782        let lng3 = -5.0;
783        let lat4 = 5.0;
784        let lng4 = -5.0;
785
786        assert!(bbox1.contains_point(MapPoint::from_lat_lng_deg(lat1, lng1)));
787        assert!(!bbox2.contains_point(MapPoint::from_lat_lng_deg(lat1, lng1)));
788        assert!(!bbox3.contains_point(MapPoint::from_lat_lng_deg(lat1, lng1)));
789        assert!(!bbox4.contains_point(MapPoint::from_lat_lng_deg(lat1, lng1)));
790
791        assert!(!bbox1.contains_point(MapPoint::from_lat_lng_deg(lat2, lng2)));
792        assert!(bbox2.contains_point(MapPoint::from_lat_lng_deg(lat2, lng2)));
793        assert!(!bbox3.contains_point(MapPoint::from_lat_lng_deg(lat2, lng2)));
794        assert!(!bbox4.contains_point(MapPoint::from_lat_lng_deg(lat2, lng2)));
795
796        assert!(!bbox1.contains_point(MapPoint::from_lat_lng_deg(lat3, lng3)));
797        assert!(!bbox2.contains_point(MapPoint::from_lat_lng_deg(lat3, lng3)));
798        assert!(bbox3.contains_point(MapPoint::from_lat_lng_deg(lat3, lng3)));
799        assert!(!bbox4.contains_point(MapPoint::from_lat_lng_deg(lat3, lng3)));
800
801        assert!(!bbox1.contains_point(MapPoint::from_lat_lng_deg(lat4, lng4)));
802        assert!(!bbox2.contains_point(MapPoint::from_lat_lng_deg(lat4, lng4)));
803        assert!(!bbox3.contains_point(MapPoint::from_lat_lng_deg(lat4, lng4)));
804        assert!(bbox4.contains_point(MapPoint::from_lat_lng_deg(lat4, lng4)));
805    }
806
807    // ---- BENCHMARKS ---- //
808    //
809    // To run the benchmarks you need Rust nightly.
810    // Add the following to lib.rs:
811    //
812    // ```
813    // #![feature(test)]
814    //
815    // #[cfg(test)]
816    // extern crate test;
817    // ```
818    //
819    // Then comment out the following:
820    //
821    // use crate::test::Bencher;
822    //
823    // fn random_map_point<T: rand::Rng>(rng: &mut T) -> MapPoint {
824    //     let lat = rng.gen_range(LatCoord::min().to_deg(),
825    // LatCoord::max().to_deg());     let lng =
826    // rng.gen_range(LngCoord::min().to_deg(), LngCoord::max().to_deg());
827    //     MapPoint::from_lat_lng_deg(lat, lng)
828    // }
829    //
830    // #[bench]
831    // fn bench_distance_of_100_000_map_points(b: &mut Bencher) {
832    //     let mut rng = rand::thread_rng();
833    //     b.iter(|| {
834    //         for _ in 0..100_000 {
835    //             let p1 = random_map_point(&mut rng);
836    //             let p2 = random_map_point(&mut rng);
837    //             let d = MapPoint::distance(p1, p2);
838    //             assert!(d.unwrap().to_meters() >= 0.0);
839    //         }
840    //     });
841    // }
842}