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