Skip to main content

lat_long/
coord.rs

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