Skip to main content

lat_long/
coord.rs

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