Skip to main content

lat_long/
alt.rs

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