lat_long/lib.rs
1//! Geographic latitude and longitude coordinate types.
2//!
3//! This crate provides strongly-typed [`Latitude`], [`Longitude`], and [`Coordinate`] values that are validated on
4//! construction and carry their own display logic (decimal degrees **or** degrees–minutes–seconds). The goal is not
5//! to provide a single, large, and potentially unwieldy "geo" crate, but rather a collection of small, focused crates
6//! that can be used together or independently.
7//!
8//! ## Quick start
9//!
10//! ```rust
11//! use lat_long::{Angle, Coordinate, Latitude, Longitude};
12//!
13//! let lat = Latitude::new(48, 51, 29.6).expect("valid latitude");
14//! let lon = Longitude::new(2, 21, 7.6).expect("valid longitude");
15//! let paris = Coordinate::new(lat, lon);
16//!
17//! // Decimal-degree display (default)
18//! println!("{paris}"); // => 48.858222, 2.218778
19//! // Degrees–minutes–seconds display (alternate flag)
20//! println!("{paris:#}"); // => 48° 51' 29.6" N, 2° 21' 7.6" E
21//! ```
22//!
23//! ```rust
24//! use lat_long::{parse::{self, Parsed}, Coordinate};
25//!
26//! if let Ok(Parsed::Coordinate(london)) = parse::parse_str("51.522, -0.127") {
27//! println!("{london}"); // => 51.522, -0.127
28//! }
29//! ```
30//!
31//! ```rust,ignore
32//! // Convert to URL, requires `url` feature flag
33//! let url = url::Url::from(paris);
34//! println!("{url}"); // => geo:48.858222,2.218778
35//! ```
36//!
37//! ```rust,ignore
38//! // Convert to JSON, requires `geojson` feature flag
39//! let json = serde_json::Value::from(paris);
40//! println!("{json}"); // => { "type": "Point", "coordinates": [48.858222,2.218778] }
41//! ```
42//!
43//! ## Formatting
44//!
45//! The [`fmt`] module provides functionality for formatting and parsing coordinates.
46//!
47//! | `FormatKind` | Format String | Positive | Negative |
48//! |-----------------|---------------|----------------------|----------------------|
49//! | `Decimal` | `{}` | 48.858222 | -48.858222 |
50//! | `DmsSigned` | `{:#}` | 48° 51′ 29.600000″ | -48° 51′ 29.600000″ |
51//! | `DmsLabeled` | N/A | 48° 51′ 29.600000″ N | 48° 51′ 29.600000″ S |
52//! | `DmsBare` | N/A | +048:51:29.600000 | -048:51:29.600000 |
53//!
54//! Note that the `DmsBare` format is intended as a regular, easy-to-parse format for use in
55//! data files, rather than as a human-readable format. In it`s coordinate pair form, it is
56//! also the only format that does not allow whitespace around the comma separator.
57//!
58//! ## Parsing
59//!
60//! The [`parse`] module provides functionality for parsing coordinates. The parser accepts all of the
61//! formats described above. The parser is also used by the implementation of `FromStr` for `Latitude`,
62//! `Longitude`, and `Coordinate`.
63//!
64//! ## Feature flags
65#![doc = document_features::document_features!()]
66//! ## References
67//!
68//! * [Latitude and longitude](https://en.wikipedia.org/wiki/Geographic_coordinate_system#Latitude_and_longitude)
69//! * [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)
70
71use ordered_float::OrderedFloat;
72use std::fmt::{Debug, Display};
73use std::hash::Hash;
74
75// ---------------------------------------------------------------------------
76// Public Types
77// ---------------------------------------------------------------------------
78
79///
80/// Shared interface for angular geographic values ([`Latitude`] and [`Longitude`]).
81///
82/// `Angle` captures the operations that apply to both kinds of geographic
83/// angle: construction from degrees–minutes–seconds, conversion to and from
84/// decimal degrees and radians, and the various flavours of absolute value
85/// (mirroring those provided by Rust's primitive integer types).
86///
87/// Implementations are guaranteed to be `Copy`, totally ordered (via
88/// [`OrderedFloat`]), and hashable — making them suitable as keys in
89/// `BTreeMap`/`HashMap` collections.
90///
91/// # Examples
92///
93/// ```rust
94/// use lat_long::{Angle, Latitude};
95///
96/// // Construct from degrees / minutes / seconds.
97/// let lat = Latitude::new(45, 30, 0.0).expect("valid latitude");
98///
99/// // Decompose back into components.
100/// assert_eq!(lat.degrees(), 45);
101/// assert_eq!(lat.minutes(), 30);
102///
103/// // Convert to and from radians.
104/// let radians = lat.to_radians();
105/// let round_trip = Latitude::from_radians(radians).unwrap();
106/// assert_eq!(lat, round_trip);
107/// ```
108///
109pub trait Angle:
110 Clone
111 + Copy
112 + Debug
113 + Default
114 + Display
115 + PartialEq
116 + Eq
117 + PartialOrd
118 + Ord
119 + Hash
120 + TryFrom<f64, Error = Error>
121 + TryFrom<OrderedFloat<f64>, Error = Error>
122 + Into<OrderedFloat<f64>>
123 + Into<f64>
124{
125 ///
126 /// The minimum legal value of this angle.
127 ///
128 /// For [`Latitude`] this is `-90°`; for [`Longitude`] this is `-180°`.
129 ///
130 const MIN: Self;
131
132 ///
133 /// The maximum legal value of this angle.
134 ///
135 /// For [`Latitude`] this is `+90°`; for [`Longitude`] this is `+180°`.
136 ///
137 const MAX: Self;
138
139 ///
140 /// Construct a new angle from degrees, minutes, and seconds.
141 ///
142 /// For negative angles, only `degrees` carries the sign — `minutes` and
143 /// `seconds` are always non-negative.
144 ///
145 /// # Errors
146 ///
147 /// Returns an [`Error`] variant if any component is out of range:
148 /// [`Error::InvalidLatitudeDegrees`] / [`Error::InvalidLongitudeDegrees`]
149 /// for an out-of-range degrees value, [`Error::InvalidMinutes`] for
150 /// `minutes ≥ 60`, or [`Error::InvalidSeconds`] for `seconds < 0.0` or
151 /// `seconds ≥ 60.0`.
152 ///
153 /// # Examples
154 ///
155 /// ```rust
156 /// use lat_long::{Angle, Latitude, Longitude};
157 ///
158 /// let lat = Latitude::new(45, 30, 0.0).unwrap();
159 /// let lon = Longitude::new(-122, 19, 59.0).unwrap();
160 /// assert!(lat.is_nonzero_positive());
161 /// assert!(lon.is_nonzero_negative());
162 ///
163 /// // Out-of-range values are rejected.
164 /// assert!(Latitude::new(91, 0, 0.0).is_err());
165 /// assert!(Longitude::new(0, 60, 0.0).is_err());
166 /// ```
167 ///
168 fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error>
169 where
170 Self: Sized;
171
172 ///
173 /// Returns the underlying decimal-degree value as an [`OrderedFloat<f64>`].
174 ///
175 /// Useful when interoperating with collections that require total ordering
176 /// (e.g. `BTreeMap`) or with APIs that use [`OrderedFloat`] directly.
177 ///
178 fn as_float(&self) -> OrderedFloat<f64> {
179 (*self).into()
180 }
181
182 ///
183 /// Returns the angle converted from decimal degrees to radians.
184 ///
185 /// # Examples
186 ///
187 /// ```rust
188 /// use lat_long::{Angle, Latitude};
189 ///
190 /// let lat = Latitude::new(180 / 2, 0, 0.0).unwrap(); // 90°
191 /// // 90° is π/2 radians.
192 /// assert!((lat.to_radians() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
193 /// ```
194 ///
195 fn to_radians(&self) -> f64 {
196 self.as_float().0.to_radians()
197 }
198
199 ///
200 /// Construct an angle from a value in radians.
201 ///
202 /// # Errors
203 ///
204 /// Returns an [`Error`] if the resulting decimal-degree value is outside
205 /// the legal range for the target type, or is not finite.
206 ///
207 /// # Examples
208 ///
209 /// ```rust
210 /// use lat_long::{Angle, Latitude};
211 ///
212 /// let lat = Latitude::from_radians(std::f64::consts::FRAC_PI_4).unwrap();
213 /// assert!((f64::from(lat) - 45.0).abs() < 1e-12);
214 /// ```
215 ///
216 fn from_radians(radians: f64) -> Result<Self, Error>
217 where
218 Self: Sized,
219 {
220 Self::try_from(OrderedFloat(radians.to_degrees()))
221 }
222
223 ///
224 /// Returns `true` if the angle is exactly zero.
225 ///
226 fn is_zero(&self) -> bool {
227 self.as_float() == inner::ZERO
228 }
229
230 ///
231 /// Returns `true` if the angle is positive and non-zero.
232 ///
233 fn is_nonzero_positive(&self) -> bool {
234 !self.is_zero() && self.as_float() > inner::ZERO
235 }
236
237 ///
238 /// Returns `true` if the angle is negative and non-zero.
239 ///
240 fn is_nonzero_negative(&self) -> bool {
241 !self.is_zero() && self.as_float() < inner::ZERO
242 }
243
244 ///
245 /// The signed integer degrees component (carries the sign for negative angles).
246 ///
247 fn degrees(&self) -> i32 {
248 inner::to_degrees_minutes_seconds(self.as_float()).0
249 }
250
251 ///
252 /// The unsigned minutes component (always in `0..60`).
253 ///
254 fn minutes(&self) -> u32 {
255 inner::to_degrees_minutes_seconds(self.as_float()).1
256 }
257
258 ///
259 /// The unsigned seconds component (always in `0.0..60.0`).
260 ///
261 fn seconds(&self) -> f32 {
262 inner::to_degrees_minutes_seconds(self.as_float()).2
263 }
264
265 ///
266 /// Returns the absolute value of this angle.
267 ///
268 /// Because both `-MIN` and `+MAX` round-trip safely for [`Latitude`] and
269 /// [`Longitude`] (they are symmetric about zero), this method never panics
270 /// for valid input. For symmetry with the primitive-integer `*_abs`
271 /// family, prefer [`checked_abs`](Self::checked_abs),
272 /// [`saturating_abs`](Self::saturating_abs), etc., when you want to
273 /// document overflow intent explicitly.
274 ///
275 /// # Examples
276 ///
277 /// ```rust
278 /// use lat_long::{Angle, Latitude};
279 ///
280 /// let south = Latitude::new(-30, 0, 0.0).unwrap();
281 /// assert_eq!(south.abs(), Latitude::new(30, 0, 0.0).unwrap());
282 /// ```
283 ///
284 fn abs(self) -> Self
285 where
286 Self: Sized,
287 {
288 Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap()
289 }
290
291 ///
292 /// Returns this angle taken modulo [`MAX`](Self::MAX).
293 ///
294 /// Useful for wrapping a longitude into the canonical `(-180°, +180°]`
295 /// range, or a latitude into `(-90°, +90°]`.
296 ///
297 /// # Examples
298 ///
299 /// ```rust
300 /// use lat_long::{Angle, Longitude};
301 ///
302 /// // 180° is already at the antimeridian; modulo MAX yields 0°.
303 /// let lon = Longitude::new(180, 0, 0.0).unwrap();
304 /// assert!(lon.modulo_max().is_zero());
305 /// ```
306 ///
307 fn modulo_max(self) -> Self
308 where
309 Self: Sized,
310 {
311 Self::try_from(self.as_float() % Self::MAX.as_float()).unwrap()
312 }
313
314 ///
315 /// Checked absolute value. Computes `self.abs()`, returning `None` if `self == MIN`.
316 ///
317 fn checked_abs(self) -> Option<Self>
318 where
319 Self: Sized,
320 {
321 if self == Self::MIN {
322 None
323 } else {
324 Some(Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap())
325 }
326 }
327
328 ///
329 /// Computes the absolute value of self.
330 ///
331 /// Returns a tuple of the absolute version of `self` along with a boolean
332 /// indicating whether an overflow happened. If `self` is the minimum value
333 /// `Self::MIN``, then the minimum value will be returned again and `true`
334 /// will be returned for an overflow happening.
335 ///
336 fn overflowing_abs(self) -> (Self, bool)
337 where
338 Self: Sized,
339 {
340 if self == Self::MIN {
341 (self, true)
342 } else {
343 (
344 Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap(),
345 false,
346 )
347 }
348 }
349
350 ///
351 /// Saturating absolute value. Computes `self.abs()`, returning `MAX`
352 /// if `self == MIN` instead of overflowing.
353 ///
354 fn saturating_abs(self) -> Self
355 where
356 Self: Sized,
357 {
358 if self == Self::MIN {
359 Self::MAX
360 } else {
361 Self::try_from(OrderedFloat(self.as_float().abs())).unwrap()
362 }
363 }
364
365 ///
366 /// Strict absolute value. Computes `self.abs()`, panicking if `self == MIN`.
367 ///
368 fn strict_abs(self) -> Self
369 where
370 Self: Sized,
371 {
372 if self == Self::MIN {
373 panic!("attempt to take absolute value of the minimum value")
374 } else {
375 Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap()
376 }
377 }
378
379 ///
380 /// Unchecked absolute value. Computes s`elf.abs()`, assuming overflow cannot occur.
381 ///
382 /// Calling `x.unchecked_abs() `is semantically equivalent to calling
383 /// `x.checked_abs().unwrap_unchecked()`.
384 ///
385 /// If you’re just trying to avoid the panic in debug mode, then do not use
386 /// this. Instead, you’re looking for `wrapping_abs`.
387 ///
388 fn unchecked_abs(self) -> Self
389 where
390 Self: Sized,
391 {
392 Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap()
393 }
394
395 ///
396 /// Wrapping (modular) absolute value. Computes `self.abs()`, wrapping around at
397 /// the boundary of the type.
398 ///
399 /// The only case where such wrapping can occur is when one takes the absolute
400 /// value of the negative minimal value for the type; this is a positive value
401 /// that is too large to represent in the type. In such a case, this function
402 /// returns `MIN` itself.
403 ///
404 fn wrapping_abs(self) -> Self
405 where
406 Self: Sized,
407 {
408 if self == Self::MIN {
409 Self::MIN
410 } else {
411 Self::try_from(OrderedFloat(self.as_float().0.abs())).unwrap()
412 }
413 }
414}
415
416// ---------------------------------------------------------------------------
417// Internal Modules
418// ---------------------------------------------------------------------------
419
420mod inner;
421pub mod parse;
422
423// ---------------------------------------------------------------------------
424// Public Modules & Exports
425// ---------------------------------------------------------------------------
426
427#[cfg(feature = "elevation")]
428pub mod elevation;
429#[cfg(feature = "elevation")]
430pub use elevation::{CoordinateWithElevation, Elevation};
431
432pub mod coord;
433pub use coord::Coordinate;
434pub mod error;
435pub use error::Error;
436pub mod fmt;
437pub mod latitude;
438pub use latitude::Latitude;
439pub mod longitude;
440pub use longitude::Longitude;