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°, with 15° 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° west of
119 /// this will have noon at 4 minutes after standard time noon, and
120 /// conversely, a user who is 1° east of the 15° 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°. 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}