1use crate::{
4 Angle, Error,
5 fmt::{FormatOptions, Formatter, formatter_impl},
6 inner,
7 parse::{self, Parsed, Value},
8};
9use core::{
10 fmt::{Debug, Display, Write},
11 str::FromStr,
12};
13use ordered_float::OrderedFloat;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub struct Latitude(OrderedFloat<f64>);
41
42pub const NORTH_POLE: Latitude = Latitude(OrderedFloat(LATITUDE_LIMIT));
48
49pub const ARCTIC_CIRCLE: Latitude = Latitude(OrderedFloat(66.5));
54
55pub const TROPIC_OF_CANCER: Latitude = Latitude(OrderedFloat(23.5));
60
61pub const EQUATOR: Latitude = Latitude(inner::ZERO);
66
67pub const TROPIC_OF_CAPRICORN: Latitude = Latitude(OrderedFloat(-23.5));
72
73pub const ANTARCTIC_CIRCLE: Latitude = Latitude(OrderedFloat(-66.5));
78
79pub const SOUTH_POLE: Latitude = Latitude(OrderedFloat(-LATITUDE_LIMIT));
81
82#[macro_export]
87macro_rules! lat {
88 (N $degrees:expr, $minutes:expr, $seconds:expr) => {
89 lat!($degrees.abs(), $minutes, $seconds).unwrap()
90 };
91 (S $degrees:expr, $minutes:expr, $seconds:expr) => {
92 lat!(-$degrees.abs(), $minutes, $seconds).unwrap()
93 };
94 ($degrees:expr, $minutes:expr, $seconds:expr) => {
95 Latitude::new($degrees, $minutes, $seconds).unwrap()
96 };
97 (N $degrees:expr, $minutes:expr) => {
98 lat!($degrees.abs(), $minutes).unwrap()
99 };
100 (S $degrees:expr, $minutes:expr) => {
101 lat!(-$degrees.abs(), $minutes).unwrap()
102 };
103 ($degrees:expr, $minutes:expr) => {
104 lat!($degrees, $minutes, 0.0).unwrap()
105 };
106 (N $degrees:expr) => {
107 lat!($degrees.abs()).unwrap()
108 };
109 (S $degrees:expr) => {
110 lat!(-$degrees.abs()).unwrap()
111 };
112 ($degrees:expr) => {
113 lat!($degrees, 0, 0.0).unwrap()
114 };
115}
116
117const LATITUDE_LIMIT: f64 = 90.0;
122
123impl Default for Latitude {
124 fn default() -> Self {
125 EQUATOR
126 }
127}
128
129impl TryFrom<f64> for Latitude {
130 type Error = Error;
131
132 fn try_from(value: f64) -> Result<Self, Self::Error> {
133 Self::try_from(OrderedFloat(value))
134 }
135}
136
137impl TryFrom<OrderedFloat<f64>> for Latitude {
138 type Error = Error;
139
140 fn try_from(value: OrderedFloat<f64>) -> Result<Self, Self::Error> {
141 if value.0 < -LATITUDE_LIMIT || value.0 > LATITUDE_LIMIT {
142 return Err(Error::InvalidAngle(value.into_inner(), LATITUDE_LIMIT));
143 }
144 Ok(Self(value))
145 }
146}
147
148impl From<Latitude> for OrderedFloat<f64> {
149 fn from(value: Latitude) -> Self {
150 value.0.into()
151 }
152}
153
154impl From<Latitude> for f64 {
155 fn from(value: Latitude) -> Self {
156 value.0.into()
157 }
158}
159
160impl FromStr for Latitude {
161 type Err = Error;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 match parse::parse_str(s)? {
165 Parsed::Angle(Value::Unknown(decimal)) => Self::try_from(decimal),
166 Parsed::Angle(Value::Latitude(lat)) => Ok(lat),
167 _ => Err(Error::InvalidAngle(0.0, 0.0)),
168 }
169 }
170}
171
172impl Display for Latitude {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 if f.alternate() {
177 let mut buf = String::new();
178 self.format(&mut buf, &FormatOptions::dms_signed())?;
179 f.write_str(&buf)
180 } else {
181 Display::fmt(&(self.0), f)
182 }
183 }
184}
185
186impl Formatter for Latitude {
187 fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
188 let fmt = (*fmt).with_labels(('N', 'S'));
189 formatter_impl(self.0, f, &fmt)
190 }
191}
192
193impl Angle for Latitude {
194 const MIN: Self = Self(OrderedFloat(-LATITUDE_LIMIT));
195 const MAX: Self = Self(OrderedFloat(LATITUDE_LIMIT));
196
197 fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error> {
198 if degrees < Self::MIN.as_float().0 as i32 || degrees > Self::MAX.as_float().0 as i32 {
199 return Err(Error::InvalidLatitudeDegrees(degrees));
200 }
201 let float = inner::from_degrees_minutes_seconds(degrees, minutes, seconds)?;
206 Self::try_from(float).map_err(|_| Error::InvalidLatitudeDegrees(degrees))
207 }
208
209 fn as_float(&self) -> OrderedFloat<f64> {
210 self.0
211 }
212}
213
214impl Latitude {
215 #[must_use]
217 pub fn is_on_equator(&self) -> bool {
218 self.is_zero()
219 }
220
221 #[must_use]
223 pub fn is_northern(&self) -> bool {
224 self.is_nonzero_positive()
225 }
226
227 #[must_use]
229 pub fn is_southern(&self) -> bool {
230 self.is_nonzero_negative()
231 }
232
233 #[must_use]
235 pub fn is_arctic(&self) -> bool {
236 *self >= ARCTIC_CIRCLE
237 }
238
239 #[must_use]
241 pub fn is_antarctic(&self) -> bool {
242 *self <= ANTARCTIC_CIRCLE
243 }
244
245 #[must_use]
250 pub fn is_tropic_of_cancer(&self) -> bool {
251 *self >= TROPIC_OF_CANCER
252 }
253
254 #[must_use]
256 pub fn is_tropic_of_capricorn(&self) -> bool {
257 *self <= TROPIC_OF_CAPRICORN
258 }
259
260 #[must_use]
267 pub fn is_tropical(&self) -> bool {
268 self.is_tropic_of_cancer() || self.is_tropic_of_capricorn()
269 }
270
271 #[must_use]
274 pub fn is_polar(&self) -> bool {
275 self.is_arctic() || self.is_antarctic()
276 }
277}