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