Skip to main content

lat_long/
longitude.rs

1//! This module provides the [`Longitude`] type, [`crate::long!`] macro, and associated constants.
2//!
3//! Longitude is a geographic coordinate that specifies the east-west position of a point on the
4//! surface of the Earth. It is an angular measurement, usually expressed in degrees and denoted
5//! by the Greek letter lambda (λ). Meridians are imaginary semicircular lines running from pole
6//! to pole that connect points with the same longitude. The prime meridian defines 0° longitude;
7//! by convention the International Reference Meridian for the Earth passes near the Royal
8//! Observatory in Greenwich, south-east London on the island of Great Britain. Positive longitudes
9//! are east of the prime meridian, and negative ones are west.
10//!
11//! The longitude denoted by the type [`Longitude`] is not strictly a *Geodetic Longitude* in that it
12//! is not defined in relation to some reference geodetic datum but some abstract center of mass.
13//!
14
15use crate::{
16    Angle, Error,
17    fmt::{FormatOptions, Formatter, formatter_impl},
18    inner,
19    parse::{self, Parsed, Value},
20};
21use core::{
22    fmt::{Debug, Display, Write},
23    str::FromStr,
24};
25use ordered_float::OrderedFloat;
26
27#[cfg(feature = "serde")]
28use serde::{Deserialize, Serialize};
29
30// ---------------------------------------------------------------------------
31// Public Types
32// ---------------------------------------------------------------------------
33
34///
35/// A geographic longitude value, constrained to **−180 ≤ degrees ≤ 180**.
36///
37/// Positive values are east of the international reference meridian; negative
38/// values are west.
39///
40/// # Examples
41///
42/// ```rust
43/// use lat_long::{Angle, Longitude};
44///
45/// let lon = Longitude::new(-73, 56, 0.0).unwrap();
46/// assert!(lon.is_western());
47/// ```
48///
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
51pub struct Longitude(OrderedFloat<f64>);
52
53// ---------------------------------------------------------------------------
54// Public Constants
55// ---------------------------------------------------------------------------
56
57///
58/// IERS International Reference Meridian (IRM), or Prime Meridian, at 0° longitude.
59///
60pub const INTERNATIONAL_REFERENCE_MERIDIAN: Longitude = Longitude(inner::ZERO);
61
62///
63/// Antimeridian, the basis for the International Date Line (IDL), at 180° longitude.
64///
65pub const ANTI_MERIDIAN: Longitude = Longitude(OrderedFloat(LONGITUDE_LIMIT));
66
67// ---------------------------------------------------------------------------
68// Public Macros
69// ---------------------------------------------------------------------------
70
71///
72/// Ergonomic constructor for [`Longitude`] values.
73///
74/// All forms `.unwrap()` internally — they are intended for compile-time-known
75/// constants and tests where invalid input is a bug. Use [`Longitude::new`]
76/// when you need to handle validation errors.
77///
78/// | Form                                | Example                   | Meaning              |
79/// |-------------------------------------|---------------------------|----------------------|
80/// | `long!(d)`                          | `long!(2)`                | 2° E                 |
81/// | `long!(d, m)`                       | `long!(2, 21)`            | 2° 21′ E             |
82/// | `long!(d, m, s)`                    | `long!(2, 21, 8.0)`       | 2° 21′ 8″ E          |
83/// | `long!(E d, …)` / `long!(W d, …)`   | `long!(W 73, 59, 8.4)`    | explicit hemisphere  |
84///
85/// The `E`/`W` prefix forms take the absolute value of the degree argument
86/// and apply the sign matching the direction.
87///
88/// # Examples
89///
90/// ```rust
91/// use lat_long::{Angle, Longitude, long};
92///
93/// let lon = long!(2, 21, 8.0);
94/// assert!(lon.is_eastern());
95/// assert_eq!(lon.degrees(), 2);
96/// ```
97///
98#[macro_export]
99macro_rules! long {
100    (E $degrees:expr, $minutes:expr, $seconds:expr) => {
101        long!($degrees.abs(), $minutes, $seconds).unwrap()
102    };
103    (W $degrees:expr, $minutes:expr, $seconds:expr) => {
104        long!(-$degrees.abs(), $minutes, $seconds).unwrap()
105    };
106    ($degrees:expr, $minutes:expr, $seconds:expr) => {
107        Longitude::new($degrees, $minutes, $seconds).unwrap()
108    };
109    (E $degrees:expr, $minutes:expr) => {
110        long!($degrees.abs(), $minutes).unwrap()
111    };
112    (W $degrees:expr, $minutes:expr) => {
113        long!(-$degrees.abs(), $minutes).unwrap()
114    };
115    ($degrees:expr, $minutes:expr) => {
116        long!($degrees, $minutes, 0.0).unwrap()
117    };
118    (E $degrees:expr) => {
119        long!($degrees.abs()).unwrap()
120    };
121    (W $degrees:expr) => {
122        long!(-$degrees.abs()).unwrap()
123    };
124    ($degrees:expr) => {
125        long!($degrees, 0, 0.0).unwrap()
126    };
127}
128
129// ---------------------------------------------------------------------------
130// Implementations
131// ---------------------------------------------------------------------------
132
133const LONGITUDE_LIMIT: f64 = 180.0;
134
135impl Default for Longitude {
136    fn default() -> Self {
137        INTERNATIONAL_REFERENCE_MERIDIAN
138    }
139}
140
141impl TryFrom<f64> for Longitude {
142    type Error = Error;
143
144    fn try_from(value: f64) -> Result<Self, Self::Error> {
145        Self::try_from(OrderedFloat(value))
146    }
147}
148
149impl TryFrom<OrderedFloat<f64>> for Longitude {
150    type Error = Error;
151
152    fn try_from(value: OrderedFloat<f64>) -> Result<Self, Self::Error> {
153        if value.is_infinite() || value.is_nan() {
154            Err(Error::InvalidNumericValue(value.into()))
155        } else if value.0 < -LONGITUDE_LIMIT || value.0 > LONGITUDE_LIMIT {
156            Err(Error::InvalidAngle(value.into_inner(), LONGITUDE_LIMIT))
157        } else {
158            Ok(Self(value))
159        }
160    }
161}
162
163impl From<Longitude> for OrderedFloat<f64> {
164    fn from(value: Longitude) -> Self {
165        value.0
166    }
167}
168
169impl From<Longitude> for f64 {
170    fn from(value: Longitude) -> Self {
171        value.0.into()
172    }
173}
174
175impl FromStr for Longitude {
176    type Err = Error;
177
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        match parse::parse_str(s)? {
180            Parsed::Angle(Value::Unknown(decimal)) => Self::try_from(decimal),
181            Parsed::Angle(Value::Longitude(lon)) => Ok(lon),
182            _ => Err(Error::InvalidAngle(0.0, 0.0)),
183        }
184    }
185}
186
187impl Display for Longitude {
188    ///
189    /// Formats the longitude as decimal degrees by default, or as
190    /// degrees–minutes–seconds when the alternate flag (`{:#}`) is used.
191    ///
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        if f.alternate() {
194            let mut buf = String::new();
195            self.format(&mut buf, &FormatOptions::dms_signed())?;
196            f.write_str(&buf)
197        } else {
198            Display::fmt(&(self.0), f)
199        }
200    }
201}
202
203impl Formatter for Longitude {
204    fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
205        let fmt = (*fmt).with_labels(('E', 'W'));
206        formatter_impl(self.0, f, &fmt)
207    }
208}
209
210impl Angle for Longitude {
211    const MIN: Self = Self(OrderedFloat(-LONGITUDE_LIMIT));
212    const MAX: Self = Self(OrderedFloat(LONGITUDE_LIMIT));
213
214    fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error> {
215        if degrees < Self::MIN.as_float().0 as i32 || degrees > Self::MAX.as_float().0 as i32 {
216            return Err(Error::InvalidLongitudeDegrees(degrees));
217        }
218        let float = inner::from_degrees_minutes_seconds(degrees, minutes, seconds)?;
219        Self::try_from(float).map_err(|_| Error::InvalidLongitudeDegrees(degrees))
220    }
221
222    fn as_float(&self) -> OrderedFloat<f64> {
223        self.0
224    }
225}
226
227impl Longitude {
228    ///
229    /// Returns `true` if this longitude is exactly on the IERS International Reference Meridian (IRM), or 0°.
230    ///
231    #[must_use]
232    pub fn is_on_international_reference_meridian(&self) -> bool {
233        self.is_zero()
234    }
235
236    ///
237    /// Returns `true` if this longitude is in the western hemisphere (< 0°).
238    ///
239    #[must_use]
240    pub fn is_western(&self) -> bool {
241        self.is_nonzero_negative()
242    }
243
244    ///
245    /// Returns `true` if this longitude is in the eastern hemisphere (> 0°).
246    ///
247    #[must_use]
248    pub fn is_eastern(&self) -> bool {
249        self.is_nonzero_positive()
250    }
251    ///
252    /// Return the [UTM longitude zone](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system#UTM_zone)
253    /// number (1–60) covering this longitude.
254    ///
255    /// Zones are 6° wide and are numbered starting at the antimeridian
256    /// (180° W = zone 1) and increasing eastward.
257    ///
258    /// # Examples
259    ///
260    /// ```rust
261    /// use lat_long::{Angle, Longitude};
262    ///
263    /// // Seattle ~ -122.3° falls in zone 10.
264    /// let lon = Longitude::try_from(-122.3).unwrap();
265    /// assert_eq!(lon.utm_zone(), 10);
266    /// ```
267    ///
268    #[must_use]
269    pub fn utm_zone(&self) -> u8 {
270        // UTM zones are 6° wide, numbered 1–60 starting at 180°W.
271        // The formula below maps the range (−180, 180] to (0, 60], with 0 and 60 both representing the same zone.
272        let zone = ((self.0 + LONGITUDE_LIMIT) / 6.0).ceil() as u8;
273        if zone == 0 { 60 } else { zone }
274    }
275}