Skip to main content

lat_long/
elevation.rs

1//!
2//! This module provides an [`Elevation`] type, [`crate::elv!`] macro, and a [`CoordinateWithElevation`]
3//! structure which is a lat/long [`Coordinate`] with an associated elevation.
4//!
5//! The elevation of a geographic location is its height above or below a fixed reference point, most
6//! commonly a reference geoid, a mathematical model of the Earth's sea level as an equipotential
7//! gravitational surface (see Geodetic datum § Vertical datum).
8//!
9//! The reference point for the type [`Elevation`] is not defined, therefore any absolute value **must**
10//! be calculated by conversion from convention in reference to some datum.
11//!
12
13use crate::{
14    Coordinate, Error, Latitude, Longitude,
15    fmt::{FormatOptions, Formatter},
16};
17use core::hash::Hash;
18use std::{
19    fmt::{Display, Write},
20    str::FromStr,
21};
22use uom::{
23    fmt::DisplayStyle,
24    si::{f64::Length, length},
25};
26
27#[cfg(feature = "geojson")]
28use crate::Angle;
29#[cfg(feature = "geojson")]
30use crate::coord::{GEOJSON_COORDINATES_FIELD, GEOJSON_POINT_TYPE, GEOJSON_TYPE_FIELD};
31
32#[cfg(feature = "serde")]
33use serde::{Deserialize, Serialize};
34
35// ------------------------------------------------------------------------------------------------
36// Public Macros
37// ------------------------------------------------------------------------------------------------
38
39/// Quick creation of [`Elevation`] values.
40///
41/// * `elv!(10.0; cm)` create an elevation of 10 centimeters.
42/// * `elv!(10.0; m)` create an elevation of 10 meters.
43/// * `elv!(10.0; km)` create an elevation of 10 kilometers.
44/// * `elv!(10.0)` create an elevation of 10 meters.
45///
46/// # Examples
47///
48/// ```rust
49/// use lat_long::elv;
50///
51/// assert_eq!("10 m".to_string(), elv!(10.0; m).to_string());
52/// ```
53#[macro_export]
54macro_rules! elv {
55    ($value:expr; cm) => {
56        $crate::elevation::Elevation::centimeters($value)
57    };
58    ($value:expr; m) => {
59        $crate::elevation::Elevation::meters($value)
60    };
61    ($value:expr; km) => {
62        $crate::elevation::Elevation::kilometers($value)
63    };
64    ($value:expr) => {
65        $crate::elevation::Elevation::meters($value)
66    };
67}
68// ------------------------------------------------------------------------------------------------
69// Public Types
70// ------------------------------------------------------------------------------------------------
71
72///
73/// An elevation, in meters, above or below an undefined reference level.
74///
75#[allow(clippy::derive_ord_xor_partial_ord)]
76#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
77#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
78pub struct Elevation(Length);
79
80///
81/// A three dimensional geographic coordinate expressed as a (latitude, longitude, elevation) triple.
82///
83/// # Examples
84///
85/// ```rust
86/// use lat_long::{Elevation, Angle, CoordinateWithElevation, Latitude, Longitude};
87///
88/// let lat = Latitude::try_from(47.6204).unwrap();
89/// let lon = Longitude::try_from(-122.3491).unwrap();
90/// let height = Elevation::meters(226.0);
91/// let top_of_seattle_space_needle = CoordinateWithElevation::new_from(lat, lon, height);
92///
93/// println!("{top_of_seattle_space_needle}");   // decimal degrees
94/// println!("{top_of_seattle_space_needle:#}"); // degrees–minutes–seconds
95/// ```
96///
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
99pub struct CoordinateWithElevation {
100    point: Coordinate,
101    elevation: Elevation,
102}
103
104// ------------------------------------------------------------------------------------------------
105// Implementations ❯ Elevation
106// ------------------------------------------------------------------------------------------------
107
108const ELEVATION_ZERO: f64 = 0.0;
109
110impl Display for Elevation {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        if f.alternate() {
113            match self.value() {
114                0.001..0.01 => self
115                    .0
116                    .into_format_args(length::millimeter, DisplayStyle::Description)
117                    .fmt(f),
118                0.01..1.0 => self
119                    .0
120                    .into_format_args(length::centimeter, DisplayStyle::Description)
121                    .fmt(f),
122                1_000.0.. => self
123                    .0
124                    .into_format_args(length::kilometer, DisplayStyle::Description)
125                    .fmt(f),
126                _ => self
127                    .0
128                    .into_format_args(length::meter, DisplayStyle::Description)
129                    .fmt(f),
130            }
131        } else {
132            self.0
133                .into_format_args(length::meter, DisplayStyle::Abbreviation)
134                .fmt(f)
135        }
136    }
137}
138
139impl FromStr for Elevation {
140    type Err = Error;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        let length_float =
144            f64::from_str(s).map_err(|_| Error::InvalidNumericFormat(s.to_string()))?;
145        Self::try_from(length_float)
146    }
147}
148
149impl TryFrom<Length> for Elevation {
150    type Error = Error;
151
152    fn try_from(value: Length) -> Result<Self, Self::Error> {
153        Self::try_from(value.value)
154    }
155}
156
157impl TryFrom<f64> for Elevation {
158    type Error = Error;
159
160    fn try_from(value: f64) -> Result<Self, Self::Error> {
161        if value.is_finite() && !value.is_nan() {
162            Ok(Self::meters(value))
163        } else {
164            Err(Error::InvalidNumericValue(value))
165        }
166    }
167}
168
169impl Eq for Elevation {}
170
171impl Hash for Elevation {
172    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
173        self.0.value.to_bits().hash(state);
174    }
175}
176
177impl Ord for Elevation {
178    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
179        self.0.value.total_cmp(&other.0.value)
180    }
181}
182
183impl From<Elevation> for Length {
184    fn from(value: Elevation) -> Self {
185        value.0
186    }
187}
188
189impl From<Elevation> for f64 {
190    fn from(value: Elevation) -> Self {
191        value.0.value
192    }
193}
194
195impl AsRef<Length> for Elevation {
196    fn as_ref(&self) -> &Length {
197        &self.0
198    }
199}
200
201impl Elevation {
202    ///
203    /// Zero is ambiguous as an elevation, since it could represent either sea level, or the ground level
204    /// at the location of interest, or a Geodetic [vertical datum](https://en.wikipedia.org/wiki/Vertical_datum).
205    ///
206    pub fn zero() -> Self {
207        Self(Length::new::<length::meter>(ELEVATION_ZERO))
208    }
209
210    ///
211    /// Construct an elevation in centimeters.
212    ///
213    pub fn centimeters(value: f64) -> Self {
214        assert!(
215            value.is_finite() && !value.is_nan(),
216            "Invalid floating point value, `{value}`"
217        );
218        Self(Length::new::<length::centimeter>(value))
219    }
220
221    ///
222    /// Construct an elevation in meters.
223    ///
224    pub fn meters(value: f64) -> Self {
225        assert!(
226            value.is_finite() && !value.is_nan(),
227            "Invalid floating point value, `{value}`"
228        );
229        Self(Length::new::<length::meter>(value))
230    }
231
232    ///
233    /// Construct an elevation in kilometers.
234    ///
235    pub fn kilometers(value: f64) -> Self {
236        assert!(
237            value.is_finite() && !value.is_nan(),
238            "Invalid floating point value, `{value}`"
239        );
240        Self(Length::new::<length::kilometer>(value))
241    }
242
243    ///
244    /// Returns the elevation value in meters as an `f64`.
245    ///
246    pub fn value(&self) -> f64 {
247        self.0.value
248    }
249
250    ///
251    /// Returns `true` if this elevation is exactly zero.
252    ///
253    pub fn is_zero(&self) -> bool {
254        self.0.value == ELEVATION_ZERO
255    }
256}
257
258// ------------------------------------------------------------------------------------------------
259// Implementations ❯ CoordinateWithElevation
260// ------------------------------------------------------------------------------------------------
261
262impl Display for CoordinateWithElevation {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        let format = if f.alternate() {
265            FormatOptions::dms()
266        } else {
267            FormatOptions::decimal()
268        };
269        self.format(f, &format)
270    }
271}
272
273impl Formatter for CoordinateWithElevation {
274    fn format<W: Write>(&self, f: &mut W, options: &FormatOptions) -> std::fmt::Result {
275        self.point.format(f, options)?;
276        write!(f, ", {}", self.elevation)
277    }
278}
279
280impl FromStr for CoordinateWithElevation {
281    type Err = Error;
282
283    fn from_str(s: &str) -> Result<Self, Self::Err> {
284        if let Some((coordinate, elevation)) = s.rsplit_once(',') {
285            Ok(Self::new(
286                Coordinate::from_str(coordinate.trim())?,
287                Elevation::from_str(elevation.trim())?,
288            ))
289        } else {
290            Err(Error::InvalidCoordinate)
291        }
292    }
293}
294
295impl CoordinateWithElevation {
296    ///
297    /// Construct a new 3d coordinate from a 2d point and an elevation.
298    ///
299    #[must_use]
300    pub const fn new(point: Coordinate, elevation: Elevation) -> Self {
301        Self { point, elevation }
302    }
303
304    /// Construct a new 3d coordinate from a 2d point, expressed as latitude and longitude
305    /// values, and an elevation.
306    #[must_use]
307    pub const fn new_from(lat: Latitude, long: Longitude, elevation: Elevation) -> Self {
308        Self::new(Coordinate::new(lat, long), elevation)
309    }
310
311    /// Return a new 3d coordinate with the point component replaced.
312    #[must_use]
313    pub const fn with_point(mut self, point: Coordinate) -> Self {
314        self.point = point;
315        self
316    }
317
318    /// Return a new 3d coordinate with the point component replaced by a new 2d coordinate.
319    #[must_use]
320    pub const fn with_new_point(mut self, lat: Latitude, long: Longitude) -> Self {
321        self.point = Coordinate::new(lat, long);
322        self
323    }
324
325    /// Return a new `CoordinateWithElevation` with the elevation component replaced.
326    #[must_use]
327    pub const fn with_elevation(mut self, elevation: Elevation) -> Self {
328        self.elevation = elevation;
329        self
330    }
331
332    /// Returns the 2d coordinate component of this 3d coordinate.
333    #[must_use]
334    pub const fn point(&self) -> Coordinate {
335        self.point
336    }
337
338    /// Returns the elevation component of this 3d coordinate.
339    #[must_use]
340    pub const fn elevation(&self) -> Elevation {
341        self.elevation
342    }
343
344    /// Returns `true` if this coordinate lies on the equator.
345    #[must_use]
346    pub fn is_on_equator(&self) -> bool {
347        self.point.latitude().is_on_equator()
348    }
349
350    /// Returns `true` if this coordinate is in the northern hemisphere.
351    #[must_use]
352    pub fn is_northern(&self) -> bool {
353        self.point.latitude().is_northern()
354    }
355
356    /// Returns `true` if this coordinate is in the southern hemisphere.
357    #[must_use]
358    pub fn is_southern(&self) -> bool {
359        self.point.latitude().is_southern()
360    }
361
362    /// Returns `true` if this coordinate lies on the international reference meridian.
363    #[must_use]
364    pub fn is_on_international_reference_meridian(&self) -> bool {
365        self.point
366            .longitude()
367            .is_on_international_reference_meridian()
368    }
369
370    /// Returns `true` if this coordinate is in the western hemisphere.
371    #[must_use]
372    pub fn is_western(&self) -> bool {
373        self.point.longitude().is_western()
374    }
375
376    /// Returns `true` if this coordinate is in the eastern hemisphere.
377    #[must_use]
378    pub fn is_eastern(&self) -> bool {
379        self.point.longitude().is_eastern()
380    }
381
382    /// Returns `true` if this coordinate lies on the equator.
383    #[must_use]
384    pub fn is_zero_elevation(&self) -> bool {
385        self.elevation.is_zero()
386    }
387}
388
389#[cfg(feature = "geojson")]
390impl From<CoordinateWithElevation> for serde_json::Value {
391    /// See [The GeoJSON Format](https://geojson.org/).
392    fn from(coord: CoordinateWithElevation) -> Self {
393        serde_json::json!({
394            GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
395            GEOJSON_COORDINATES_FIELD: [
396                coord.point().latitude().as_float().0,
397                coord.point().longitude().as_float().0,
398                coord.elevation().value()
399            ]
400        })
401    }
402}
403
404#[cfg(feature = "geojson")]
405impl TryFrom<serde_json::Value> for CoordinateWithElevation {
406    type Error = crate::Error;
407
408    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
409        if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
410            return Err(crate::Error::InvalidCoordinate);
411        }
412        let coords = value[GEOJSON_COORDINATES_FIELD]
413            .as_array()
414            .ok_or(crate::Error::InvalidCoordinate)?;
415        if coords.len() != 3 {
416            return Err(crate::Error::InvalidCoordinate);
417        }
418        let lat_val: f64 = coords[0]
419            .as_f64()
420            .ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
421        let lon_val: f64 = coords[1]
422            .as_f64()
423            .ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
424        let alt_val: f64 = coords[2]
425            .as_f64()
426            .ok_or(crate::Error::InvalidNumericFormat(coords[2].to_string()))?;
427        let lat = Latitude::try_from(lat_val)?;
428        let lon = Longitude::try_from(lon_val)?;
429        let alt = Elevation::try_from(alt_val)?;
430        Ok(CoordinateWithElevation::new_from(lat, lon, alt))
431    }
432}