Skip to main content

lat_long/
coord.rs

1//! This module provides the [`Coordinate`] type, [`crate::coord!`] macro, and associated constants.
2
3use crate::{
4    Error, Latitude, Longitude,
5    fmt::{FormatKind, FormatOptions, Formatter},
6    lat::EQUATOR,
7    long::INTERNATIONAL_REFERENCE_MERIDIAN,
8    parse::{self, Parsed},
9};
10use core::{
11    fmt::{Debug, Display, Write},
12    str::FromStr,
13};
14
15#[cfg(feature = "serde")]
16use serde::{Deserialize, Serialize};
17
18#[cfg(feature = "geojson")]
19use crate::Angle;
20
21// ---------------------------------------------------------------------------
22// Public Types
23// ---------------------------------------------------------------------------
24
25/// A geographic coordinate expressed as a (latitude, longitude) pair.
26///
27/// # Examples
28///
29/// ```rust
30/// use lat_long::{Angle, Coordinate, Latitude, Longitude};
31///
32/// let lat = Latitude::new(51, 30, 26.0).unwrap();
33/// let lon = Longitude::new(0, 7, 39.0).unwrap();
34/// let london = Coordinate::new(lat, lon);
35///
36/// println!("{london}");   // decimal degrees
37/// println!("{london:#}"); // degrees–minutes–seconds
38/// ```
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
41pub struct Coordinate {
42    lat: Latitude,   // φ
43    long: Longitude, // λ
44}
45
46// ---------------------------------------------------------------------------
47// Public Constants
48// ---------------------------------------------------------------------------
49
50/// The URI scheme used by [`Coordinate::to_url_string`] to format a `geo:` URI.
51///
52/// Defined by [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870).
53pub const GEO_URL_SCHEME: &str = "geo";
54
55#[cfg(feature = "geojson")]
56pub const GEOJSON_TYPE_FIELD: &str = "type";
57#[cfg(feature = "geojson")]
58pub const GEOJSON_COORDINATES_FIELD: &str = "coordinates";
59#[cfg(feature = "geojson")]
60pub const GEOJSON_POINT_TYPE: &str = "Point";
61
62// ---------------------------------------------------------------------------
63// Public Macros
64// ---------------------------------------------------------------------------
65
66#[cfg(not(feature = "3d"))]
67#[macro_export]
68macro_rules! coord {
69    ($lat:expr ; $lon:expr) => {
70        $crate::coord::Coordinate::new($lat, $lon)
71    };
72}
73
74#[cfg(feature = "3d")]
75#[macro_export]
76macro_rules! coord {
77    ($lat:expr ; $lon:expr) => {
78        $crate::coord::Coordinate::new($lat, $lon)
79    };
80    ($lat:expr ; $lon:expr ; $alt:expr) => {
81        $crate::alt::Coordinate3d::new_from($lat, $lon, $alt)
82    };
83}
84
85// ---------------------------------------------------------------------------
86// Implementations
87// ---------------------------------------------------------------------------
88
89impl Default for Coordinate {
90    fn default() -> Self {
91        Self {
92            lat: EQUATOR,
93            long: INTERNATIONAL_REFERENCE_MERIDIAN,
94        }
95    }
96}
97
98impl From<(Latitude, Longitude)> for Coordinate {
99    fn from(value: (Latitude, Longitude)) -> Self {
100        Self::new(value.0, value.1)
101    }
102}
103
104impl From<Coordinate> for (Latitude, Longitude) {
105    fn from(value: Coordinate) -> Self {
106        (value.lat, value.long)
107    }
108}
109
110impl From<Latitude> for Coordinate {
111    fn from(value: Latitude) -> Self {
112        Self::new(value, Longitude::default())
113    }
114}
115
116impl From<Longitude> for Coordinate {
117    fn from(value: Longitude) -> Self {
118        Self::new(Latitude::default(), value)
119    }
120}
121
122impl FromStr for Coordinate {
123    type Err = Error;
124
125    fn from_str(s: &str) -> Result<Self, Self::Err> {
126        match parse::parse_str(s)? {
127            Parsed::Coordinate(coord) => Ok(coord),
128            _ => Err(Error::InvalidAngle(0.0, 0.0)),
129        }
130    }
131}
132
133impl Display for Coordinate {
134    /// Formats the coordinate as `"latitude, longitude"`.
135    ///
136    /// Uses decimal degrees by default; the alternate flag (`{:#}`) switches
137    /// both components to degrees–minutes–seconds.
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        let format = if f.alternate() {
140            FormatOptions::dms()
141        } else {
142            FormatOptions::decimal()
143        };
144        self.format(f, &format)
145    }
146}
147
148impl Formatter for Coordinate {
149    fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
150        let kind = fmt.kind();
151        self.lat.format(f, fmt)?;
152        write!(f, ",{}", if kind == FormatKind::DmsBare { "" } else { " " })?;
153        self.long.format(f, fmt)
154    }
155}
156
157impl Coordinate {
158    /// Construct a new `Coordinate` from a validated [`Latitude`] and [`Longitude`].
159    ///
160    /// # Examples
161    ///
162    /// ```rust
163    /// use lat_long::{Angle, Coordinate, Latitude, Longitude};
164    ///
165    /// let lat = Latitude::new(48, 51, 30.0).unwrap();
166    /// let lon = Longitude::new(2, 21, 8.0).unwrap();
167    /// let paris = Coordinate::new(lat, lon);
168    /// assert!(paris.is_northern());
169    /// assert!(paris.is_eastern());
170    /// ```
171    pub const fn new(lat: Latitude, long: Longitude) -> Self {
172        Self { lat, long }
173    }
174
175    /// Return a new `Coordinate` with the latitude component replaced.
176    #[must_use]
177    pub const fn with_latitude(mut self, lat: Latitude) -> Self {
178        self.lat = lat;
179        self
180    }
181
182    /// Return a new `Coordinate` with the longitude component replaced.
183    #[must_use]
184    pub const fn with_longitude(mut self, long: Longitude) -> Self {
185        self.long = long;
186        self
187    }
188
189    /// Returns the latitude component of this coordinate.
190    #[must_use]
191    pub const fn latitude(&self) -> Latitude {
192        self.lat
193    }
194
195    /// Returns the latitude component of this coordinate.
196    #[must_use]
197    pub const fn φ(&self) -> Latitude {
198        self.lat
199    }
200
201    /// Returns the longitude component of this coordinate.
202    #[must_use]
203    pub const fn longitude(&self) -> Longitude {
204        self.long
205    }
206
207    /// Returns the longitude component of this coordinate.
208    #[must_use]
209    pub const fn λ(&self) -> Longitude {
210        self.long
211    }
212
213    /// Returns `true` if this coordinate lies on the equator.
214    #[must_use]
215    pub fn is_on_equator(&self) -> bool {
216        self.lat.is_on_equator()
217    }
218
219    /// Returns `true` if this coordinate is in the northern hemisphere.
220    #[must_use]
221    pub fn is_northern(&self) -> bool {
222        self.lat.is_northern()
223    }
224
225    /// Returns `true` if this coordinate is in the southern hemisphere.
226    #[must_use]
227    pub fn is_southern(&self) -> bool {
228        self.lat.is_southern()
229    }
230
231    /// Returns `true` if this coordinate lies on the international reference meridian.
232    #[must_use]
233    pub fn is_on_international_reference_meridian(&self) -> bool {
234        self.long.is_on_international_reference_meridian()
235    }
236
237    /// Returns `true` if this coordinate is in the western hemisphere.
238    #[must_use]
239    pub fn is_western(&self) -> bool {
240        self.long.is_western()
241    }
242
243    /// Returns `true` if this coordinate is in the eastern hemisphere.
244    #[must_use]
245    pub fn is_eastern(&self) -> bool {
246        self.long.is_eastern()
247    }
248
249    /// Format this coordinate as a `geo:` URI string.
250    ///
251    /// The format is `geo:<lat>,<lon>` using decimal degrees with 8 places of
252    /// precision, as per [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870).
253    ///
254    /// # Examples
255    ///
256    /// ```rust
257    /// use lat_long::{Angle, Coordinate, Latitude, Longitude};
258    ///
259    /// let lat = Latitude::new(48, 51, 30.0).unwrap();
260    /// let lon = Longitude::new(2, 21, 8.0).unwrap();
261    /// let paris = Coordinate::new(lat, lon);
262    /// assert!(paris.to_url_string().starts_with("geo:"));
263    /// ```
264    #[must_use]
265    pub fn to_url_string(&self) -> String {
266        format!(
267            "{}:{},{}",
268            GEO_URL_SCHEME,
269            self.lat.to_formatted_string(&FormatOptions::decimal()),
270            self.long.to_formatted_string(&FormatOptions::decimal())
271        )
272    }
273
274    /// Format this coordinate as a microformat string.
275    ///
276    /// This follows the microformat standard for representing coordinates specified
277    /// in [mf-geo](https://microformats.org/wiki/geo) and referenced by
278    /// [hCard](https://microformats.org/wiki/hcard) and
279    /// [hCalendar](https://microformats.org/wiki/hcalendar).
280    ///
281    /// # Examples
282    ///
283    /// ```rust
284    /// use lat_long::{Angle, Coordinate, Latitude, Longitude};
285    ///
286    /// let lat = Latitude::new(48, 51, 30.0).unwrap();
287    /// let lon = Longitude::new(2, 21, 8.0).unwrap();
288    /// let paris = Coordinate::new(lat, lon);
289    /// assert!(paris.to_microformat_string().contains("class=\"latitude\""));
290    /// assert!(paris.to_microformat_string().contains("class=\"longitude\""));
291    /// ```
292    #[must_use]
293    pub fn to_microformat_string(&self) -> String {
294        format!(
295            "<span class=\"latitude\">{}</span>; <span class=\"longitude\">{}</span>",
296            self.lat.to_formatted_string(&FormatOptions::decimal()),
297            self.long.to_formatted_string(&FormatOptions::decimal())
298        )
299    }
300}
301
302#[cfg(feature = "urn")]
303impl From<Coordinate> for url::Url {
304    fn from(coord: Coordinate) -> Self {
305        Self::parse(&coord.to_url_string()).unwrap()
306    }
307}
308
309#[cfg(feature = "urn")]
310impl TryFrom<url::Url> for Coordinate {
311    type Error = crate::Error;
312
313    fn try_from(url: url::Url) -> Result<Self, Self::Error> {
314        if url.scheme() != GEO_URL_SCHEME {
315            return Err(crate::Error::InvalidUrnScheme);
316        }
317        let path = url.path();
318        let parts: Vec<&str> = path.split(',').collect();
319        if parts.len() != 2 {
320            return Err(crate::Error::InvalidCoordinate);
321        }
322        let lat_val: f64 = parts[0]
323            .parse()
324            .map_err(|_| crate::Error::InvalidCoordinate)?;
325        let lon_val: f64 = parts[1]
326            .parse()
327            .map_err(|_| crate::Error::InvalidCoordinate)?;
328        let lat = Latitude::try_from(lat_val).map_err(|_| crate::Error::InvalidCoordinate)?;
329        let lon = Longitude::try_from(lon_val).map_err(|_| crate::Error::InvalidCoordinate)?;
330        Ok(Coordinate::new(lat, lon))
331    }
332}
333
334#[cfg(feature = "geojson")]
335impl From<Coordinate> for serde_json::Value {
336    /// See [The GeoJSON Format](https://geojson.org/).
337    fn from(coord: Coordinate) -> Self {
338        serde_json::json!({
339            GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
340            GEOJSON_COORDINATES_FIELD: [
341                coord.lat.as_float().0,
342                coord.long.as_float().0
343            ]
344        })
345    }
346}
347
348#[cfg(feature = "geojson")]
349impl TryFrom<serde_json::Value> for Coordinate {
350    type Error = crate::Error;
351
352    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
353        if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
354            return Err(crate::Error::InvalidCoordinate);
355        }
356        let coords = value[GEOJSON_COORDINATES_FIELD]
357            .as_array()
358            .ok_or(crate::Error::InvalidCoordinate)?;
359        if coords.len() != 2 {
360            return Err(crate::Error::InvalidCoordinate);
361        }
362        let lat_val: f64 = coords[0]
363            .as_f64()
364            .ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
365        let lon_val: f64 = coords[1]
366            .as_f64()
367            .ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
368        let lat = Latitude::try_from(lat_val)?;
369        let lon = Longitude::try_from(lon_val)?;
370        Ok(Coordinate::new(lat, lon))
371    }
372}