Skip to main content

rust_zmanim/util/
geolocation.rs

1//! Geolocation struct, with some math for [local mean
2//! time](crate::astronomical_calculator::local_mean_time)
3use crate::util::math_helper::HOUR_MINUTES;
4use jiff::tz::TimeZone;
5
6#[derive(Debug, Clone, PartialEq)]
7/// A struct that contains location information such as latitude and longitude
8/// required for astronomical calculations. The elevation field may be ignored
9/// by calculations that do not account for elevation.
10///
11/// Construct with [`GeoLocation::new`], which validates the coordinates.
12pub struct GeoLocation {
13    pub(crate) latitude: f64,
14    pub(crate) longitude: f64,
15    pub(crate) elevation: f64,
16    pub(crate) timezone: TimeZone,
17}
18
19/// An invalid [`GeoLocation`] parameter, returned by [`GeoLocation::new`]. The
20/// contained value is the rejected input.
21#[derive(Debug, Clone, Copy, PartialEq)]
22#[non_exhaustive]
23pub enum GeoLocationError {
24    /// Latitude was NaN or outside `[-90.0, 90.0]`
25    InvalidLatitude(f64),
26    /// Longitude was NaN or outside `[-180.0, 180.0]`
27    InvalidLongitude(f64),
28    /// Elevation was negative, NaN, or infinite
29    InvalidElevation(f64),
30}
31
32impl core::fmt::Display for GeoLocationError {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        match self {
35            Self::InvalidLatitude(lat) => {
36                write!(f, "latitude {lat} is not in the range [-90.0, 90.0]")
37            }
38            Self::InvalidLongitude(long) => {
39                write!(f, "longitude {long} is not in the range [-180.0, 180.0]")
40            }
41            Self::InvalidElevation(elev) => {
42                write!(
43                    f,
44                    "elevation {elev} is not a non-negative finite number of meters"
45                )
46            }
47        }
48    }
49}
50
51impl std::error::Error for GeoLocationError {}
52
53impl GeoLocation {
54    /// Returns a validated `GeoLocation`.
55    ///
56    /// `latitude` is in the World Geodetic System, or degrees North of the
57    /// Equator, and must be in `[-90.0, 90.0]`. `longitude` is in the World
58    /// Geodetic System, or degrees East of the IERS Reference Meridian, and
59    /// must be in `[-180.0, 180.0]`. `elevation` is in meters above sea level
60    /// and must be non-negative and finite.
61    ///
62    /// # Errors
63    ///
64    /// Returns a [`GeoLocationError`] describing the first invalid parameter.
65    pub fn new(
66        latitude: f64,
67        longitude: f64,
68        elevation: f64,
69        timezone: TimeZone,
70    ) -> Result<Self, GeoLocationError> {
71        if !(-90.0..=90.0).contains(&latitude) {
72            return Err(GeoLocationError::InvalidLatitude(latitude));
73        }
74        if !(-180.0..=180.0).contains(&longitude) {
75            return Err(GeoLocationError::InvalidLongitude(longitude));
76        }
77        if !elevation.is_finite() || elevation < 0.0 {
78            return Err(GeoLocationError::InvalidElevation(elevation));
79        }
80        Ok(Self {
81            latitude,
82            longitude,
83            elevation,
84            timezone,
85        })
86    }
87
88    /// The latitude in the World Geodetic System, or degrees North of the
89    /// Equator
90    #[must_use]
91    pub fn latitude(&self) -> f64 {
92        self.latitude
93    }
94
95    /// The longitude in the World Geodetic System, or degrees East of the IERS
96    /// Reference Meridian
97    #[must_use]
98    pub fn longitude(&self) -> f64 {
99        self.longitude
100    }
101
102    /// The elevation in meters above sea level
103    #[must_use]
104    pub fn elevation(&self) -> f64 {
105        self.elevation
106    }
107
108    /// The location's time zone
109    #[must_use]
110    pub fn timezone(&self) -> &TimeZone {
111        &self.timezone
112    }
113
114    /// Returns the location's local mean time offset from UTC in hours. The
115    /// globe is split into 360&deg;, with 15&deg; per hour of the day. For a
116    /// location that is at a longitude evenly divisible by 15 (`longitude
117    /// % 15 == 0`), at solar noon (with adjustment for the equation of time)
118    /// the sun should be directly overhead, so a user who is 1&deg; west of
119    /// this will have noon at 4 minutes after standard time noon, and
120    /// conversely, a user who is 1&deg; east of the 15&deg; longitude will have
121    /// noon at 11:56 AM. Lakewood, N.J., whose longitude is -74.222, is 0.778
122    /// away from the closest multiple of 15 at -75&deg;. This is multiplied by
123    /// 4 to yield 3 minutes and 7 seconds earlier than standard time. The
124    /// offset returned does not account for the daylight saving time offset
125    /// since this struct is unaware of dates.
126    #[must_use]
127    pub fn local_mean_time_offset(&self) -> f64 {
128        (self.longitude * 4.0) / HOUR_MINUTES
129    }
130}