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}