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