Skip to main content

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;