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
79pub trait Angle:
80 Clone
81 + Copy
82 + Debug
83 + Default
84 + Display
85 + PartialEq
86 + Eq
87 + PartialOrd
88 + Ord
89 + Hash
90 + TryFrom<OrderedFloat<f64>, Error = Error>
91 + Into<OrderedFloat<f64>>
92{
93 const MIN: Self;
94 const MAX: Self;
95
96 /// Construct a new angle from degrees, minutes, and seconds.
97 fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error>
98 where
99 Self: Sized;
100
101 fn as_float(&self) -> OrderedFloat<f64> {
102 (*self).into()
103 }
104
105 /// Returns `true` if the angle is exactly zero.
106 fn is_zero(&self) -> bool {
107 self.as_float() == inner::ZERO
108 }
109
110 /// Returns `true` if the angle is positive and non-zero.
111 fn is_nonzero_positive(&self) -> bool {
112 !self.is_zero() && self.as_float() > inner::ZERO
113 }
114
115 /// Returns `true` if the angle is negative and non-zero.
116 fn is_nonzero_negative(&self) -> bool {
117 !self.is_zero() && self.as_float() < inner::ZERO
118 }
119
120 /// The signed integer degrees component (carries the sign for negative angles).
121 fn degrees(&self) -> i32 {
122 inner::to_degrees_minutes_seconds(self.as_float()).0
123 }
124
125 /// The unsigned minutes component (always in `0..60`).
126 fn minutes(&self) -> u32 {
127 inner::to_degrees_minutes_seconds(self.as_float()).1
128 }
129
130 /// The unsigned seconds component (always in `0.0..60.0`).
131 fn seconds(&self) -> f32 {
132 inner::to_degrees_minutes_seconds(self.as_float()).2
133 }
134
135 /// Checked absolute value. Computes self.abs(), returning None if self == MIN.
136 fn checked_abs(self) -> Option<Self>
137 where
138 Self: Sized,
139 {
140 if self == Self::MIN {
141 None
142 } else {
143 Some(Self::try_from(OrderedFloat(self.into().0.abs())).unwrap())
144 }
145 }
146
147 /// Computes the absolute value of self.
148 ///
149 /// Returns a tuple of the absolute version of self along with a boolean indicating whether an overflow happened.
150 /// If self is the minimum value Self::MIN, then the minimum value will be returned again and true will be returned
151 /// for an overflow happening.
152 fn overflowing_abs(self) -> (Self, bool)
153 where
154 Self: Sized,
155 {
156 if self == Self::MIN {
157 (self, true)
158 } else {
159 (
160 Self::try_from(OrderedFloat(self.into().0.abs())).unwrap(),
161 false,
162 )
163 }
164 }
165
166 /// Saturating absolute value. Computes self.abs(), returning MAX if self == MIN instead of overflowing.
167 fn saturating_abs(self) -> Self
168 where
169 Self: Sized,
170 {
171 if self == Self::MIN {
172 Self::MAX
173 } else {
174 Self::try_from(OrderedFloat(self.into().0.abs())).unwrap()
175 }
176 }
177
178 /// Strict absolute value. Computes self.abs(), panicking if self == MIN.
179 fn strict_abs(self) -> Self
180 where
181 Self: Sized,
182 {
183 if self == Self::MIN {
184 panic!("attempt to take absolute value of the minimum value")
185 } else {
186 Self::try_from(OrderedFloat(self.into().0.abs())).unwrap()
187 }
188 }
189
190 /// Unchecked absolute value. Computes self.abs(), assuming overflow cannot occur.
191 ///
192 /// Calling x.unchecked_abs() is semantically equivalent to calling x.checked_abs().unwrap_unchecked().
193 ///
194 /// If you’re just trying to avoid the panic in debug mode, then do not use this. Instead, you’re looking for wrapping_abs.
195 fn unchecked_abs(self) -> Self
196 where
197 Self: Sized,
198 {
199 Self::try_from(OrderedFloat(self.into().0.abs())).unwrap()
200 }
201
202 /// Wrapping (modular) absolute value. Computes self.abs(), wrapping around at the boundary of the type.
203 ///
204 /// The only case where such wrapping can occur is when one takes the absolute value of the negative minimal
205 /// value for the type; this is a positive value that is too large to represent in the type. In such a case,
206 /// this function returns MIN itself.
207 fn wrapping_abs(self) -> Self
208 where
209 Self: Sized,
210 {
211 if self == Self::MIN {
212 Self::MIN
213 } else {
214 Self::try_from(OrderedFloat(self.into().0.abs())).unwrap()
215 }
216 }
217}
218
219// ---------------------------------------------------------------------------
220// Internal Modules
221// ---------------------------------------------------------------------------
222
223mod inner;
224pub mod parse;
225
226// ---------------------------------------------------------------------------
227// Public Modules & Exports
228// ---------------------------------------------------------------------------
229
230#[cfg(feature = "3d")]
231pub mod alt;
232#[cfg(feature = "3d")]
233pub use alt::{Altitude, Coordinate3d};
234
235pub mod coord;
236pub use coord::Coordinate;
237pub mod error;
238pub use error::Error;
239pub mod fmt;
240pub mod lat;
241pub use lat::Latitude;
242pub mod long;
243pub use long::Longitude;