dms_coordinates/
dms3d.rs

1//! 3D D°M'S" coordinates
2use crate::Error;
3use crate::EARTH_RADIUS;
4use crate::{projected_distance, Cardinal, DMS};
5
6#[cfg(feature = "serde")]
7use serde_derive::{Deserialize, Serialize};
8
9#[cfg(feature = "gpx")]
10use gpx::Waypoint;
11
12/// 3D D°M'S" coordinates, comprises
13/// a latitude: D°M'S" angle
14/// a longitude: D°M'S" angle
15/// and optionnal altitude
16#[derive(PartialEq, Copy, Clone, Debug)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct DMS3d {
19    /// Latitude angle in D°M'S", cardinal is mandatory
20    pub latitude: DMS,
21    /// Longitude angle in D°M'S", cardinal is mandatory
22    pub longitude: DMS,
23    /// Optionnal altitude / depth
24    pub altitude: Option<f64>,
25}
26
27impl core::fmt::Display for DMS3d {
28    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
29        write!(
30            f,
31            "lat: \"{}\"  lon: \"{}\" alt: \"{}\"",
32            self.latitude,
33            self.longitude,
34            self.altitude.unwrap_or(0.0_f64)
35        )
36    }
37}
38
39impl Default for DMS3d {
40    /// Default DMS3D with null coordinates and null altitude
41    fn default() -> Self {
42        Self {
43            latitude: DMS::from_ddeg_latitude(0.0_f64),
44            longitude: DMS::from_ddeg_longitude(0.0_f64),
45            altitude: None,
46        }
47    }
48}
49
50impl From<DMS3d> for (f64, f64) {
51    /// Converts self to (latddeg, londdeg)
52    fn from(val: DMS3d) -> Self {
53        (val.latitude.to_ddeg_angle(), val.longitude.to_ddeg_angle())
54    }
55}
56
57impl From<rust_3d::Point3D> for DMS3d {
58    /// Builds 3D D°M'S" coordinates from cartesian (ECEF) coordinates
59    fn from(item: rust_3d::Point3D) -> Self {
60        Self::from_cartesian(item)
61    }
62}
63
64impl core::ops::Add<DMS3d> for DMS3d {
65    type Output = Result<DMS3d, Error>;
66    fn add(self, rhs: Self) -> Result<Self, Error> {
67        if let Some(a0) = self.altitude {
68            if let Some(a1) = rhs.altitude {
69                Ok(DMS3d {
70                    latitude: (self.latitude + rhs.latitude)?,
71                    longitude: (self.longitude + rhs.longitude)?,
72                    altitude: Some(a1),
73                })
74            } else {
75                Ok(DMS3d {
76                    latitude: (self.latitude + rhs.latitude)?,
77                    longitude: (self.longitude + rhs.longitude)?,
78                    altitude: Some(a0),
79                })
80            }
81        } else {
82            Ok(DMS3d {
83                latitude: (self.latitude + rhs.latitude)?,
84                longitude: (self.longitude + rhs.longitude)?,
85                altitude: None,
86            })
87        }
88    }
89}
90impl DMS3d {
91    /// Builds `3D D°M'S"` coordinates
92    pub fn new(latitude: DMS, longitude: DMS, altitude: Option<f64>) -> Result<DMS3d, Error> {
93        let cardlat = latitude.cardinal.ok_or(Error::MissingLatitude)?;
94        if !cardlat.is_latitude() {
95            return Err(Error::InvalidLatitude);
96        }
97        let cardlon = longitude.cardinal.ok_or(Error::MissingLongitude)?;
98        if !cardlon.is_longitude() {
99            return Err(Error::InvalidLongitude);
100        }
101        Ok(DMS3d {
102            latitude,
103            longitude,
104            altitude,
105        })
106    }
107    /// Builds 3D DMS copy with given altitude attribute in `meters`,
108    /// if altitude data was already present, it gets overwritten
109    pub fn with_altitude(&self, altitude: f64) -> DMS3d {
110        DMS3d {
111            latitude: self.latitude,
112            longitude: self.longitude,
113            altitude: Some(altitude),
114        }
115    }
116
117    /// Same as [with_altitude] but quantity is expressed in `feet`
118    pub fn with_altitude_feet(&self, altitude: f64) -> DMS3d {
119        self.with_altitude(altitude / 3.28084)
120    }
121
122    /// Adds given altitude quantity to self,
123    /// if altitude was not defined yet, it takes this value
124    pub fn add_altitude(&mut self, altitude: f64) {
125        if let Some(a) = self.altitude {
126            self.altitude = Some(a + altitude)
127        } else {
128            self.altitude = Some(altitude)
129        }
130    }
131
132    /// Same as [add_altitude] but quantity is expressed in `feet`
133    pub fn add_altitude_feet(&mut self, altitude: f64) {
134        self.add_altitude(altitude / 3.28084)
135    }
136
137    /// Builds `3D D°M'S"` coordinates from given angles, expressed
138    /// in decimal degrees, and an optionnal altitude.
139    pub fn from_ddeg_angles(latitude: f64, longitude: f64, altitude: Option<f64>) -> DMS3d {
140        DMS3d {
141            latitude: {
142                let dms = DMS::from_ddeg_angle(latitude);
143                if latitude < 0.0 {
144                    dms.with_cardinal(Cardinal::South)
145                } else {
146                    dms.with_cardinal(Cardinal::North)
147                }
148            },
149            longitude: {
150                let dms = DMS::from_ddeg_angle(longitude);
151                if longitude < 0.0 {
152                    dms.with_cardinal(Cardinal::West)
153                } else {
154                    dms.with_cardinal(Cardinal::East)
155                }
156            },
157            altitude,
158        }
159    }
160
161    /// Builds 3D D°M'S" coordinates from given Cartesian coordinates
162    pub fn from_cartesian(xyz: rust_3d::Point3D) -> DMS3d {
163        DMS3d {
164            latitude: DMS::from_ddeg_latitude(map_3d::rad2deg((xyz.z / EARTH_RADIUS).asin())),
165            longitude: DMS::from_ddeg_longitude(map_3d::rad2deg(xyz.y.atan2(xyz.x))),
166            altitude: Some(xyz.z),
167        }
168    }
169
170    /// Returns distance in meters, between Self and given 3D D°M'S" coordinates
171    pub fn distance(&self, other: DMS3d) -> f64 {
172        projected_distance(
173            (
174                self.latitude.to_ddeg_angle(),
175                self.longitude.to_ddeg_angle(),
176            ),
177            (
178                other.latitude.to_ddeg_angle(),
179                other.longitude.to_ddeg_angle(),
180            ),
181        )
182    }
183
184    /// Returns azimuth angle ɑ, where 0 <= ɑ < 360,
185    /// between Self & other 3D D°M'S" coordinates.
186    /// ɑ, being the angle between North Pole & `rhs` coordinates
187    pub fn azimuth(&self, rhs: Self) -> f64 {
188        let (phi1, phi2) = (
189            map_3d::deg2rad(self.latitude.to_ddeg_angle()),
190            map_3d::deg2rad(rhs.latitude.to_ddeg_angle()),
191        );
192        let (lambda1, lambda2) = (
193            map_3d::deg2rad(self.longitude.to_ddeg_angle()),
194            map_3d::deg2rad(rhs.longitude.to_ddeg_angle()),
195        );
196        let dlambda = lambda2 - lambda1;
197        let y = dlambda.sin() * phi2.cos();
198        let x = phi1.cos() * phi2.sin() - phi1.sin() * phi2.cos() * dlambda.cos();
199        map_3d::rad2deg(y.atan2(x))
200    }
201
202    /// Converts Self to Cartesian Coordinates (x, y, z).
203    /// (x = 0, y = 0, z = 0) being Earth center, in Cartesian coordinates.
204    pub fn to_cartesian(&self) -> rust_3d::Point3D {
205        let (lat, lon) = (
206            map_3d::deg2rad(self.latitude.to_ddeg_angle()),
207            map_3d::deg2rad(self.longitude.to_ddeg_angle()),
208        );
209        rust_3d::Point3D {
210            x: EARTH_RADIUS * lat.cos() * lon.cos(),
211            y: EARTH_RADIUS * lat.cos() * lon.sin(),
212            z: EARTH_RADIUS * lat.sin(),
213        }
214    }
215    /// Converts Self from WGS84 to EU50 Data
216    pub fn to_europe50(&self) -> Result<DMS3d, Error> {
217        Ok(DMS3d {
218            latitude: self.latitude.to_europe50()?,
219            longitude: self.longitude.to_europe50()?,
220            altitude: self.altitude,
221        })
222    }
223}
224
225#[cfg(feature = "gpx")]
226impl From<Waypoint> for DMS3d {
227    fn from(wpt: Waypoint) -> Self {
228        Self::from_ddeg_angles(wpt.point().x(), wpt.point().y(), wpt.elevation)
229    }
230}
231
232/*
233impl core::ops::Sub for DMS3d {
234    type Output = DMS3d;
235    fn sub (self, rhs: Self) -> Self {
236        let altitude : Option<f64> = match self.altitude {
237            Some(altitude) => {
238                match rhs.altitude {
239                    Some(a) => Some(altitude - a),
240                    None => Some(altitude),
241                }
242            },
243            None => {
244                match rhs.altitude {
245                    Some(a) => Some(-a),
246                    None => None,
247                }
248            },
249        };
250        DMS3d {
251            latitude : self.latitude - rhs.latitude,
252            longitude: self.longitude - rhs.longitude,
253            altitude: altitude,
254        }
255    }
256}
257
258impl core::ops::Mul for DMS3d {
259    type Output = DMS3d;
260    fn mul (self, rhs: Self) -> Self {
261        let altitude : Option<f64> = match self.altitude {
262            Some(altitude) => {
263                match rhs.altitude {
264                    Some(a) => Some(altitude - a),
265                    None => Some(altitude),
266                }
267            },
268            None => {
269                match rhs.altitude {
270                    Some(a) => Some(-a),
271                    None => None,
272                }
273            },
274        };
275        DMS3d {
276            latitude : self.latitude * rhs.latitude,
277            longitude: self.longitude * rhs.longitude,
278            altitude: altitude,
279        }
280    }
281}
282
283impl core::ops::Div for DMS3d {
284    type Output = DMS3d;
285    fn div (self, rhs: Self) -> Self {
286        let altitude : Option<f64> = match self.altitude {
287            Some(altitude) => {
288                match rhs.altitude {
289                    Some(a) => Some(altitude - a),
290                    None => Some(altitude),
291                }
292            },
293            None => {
294                match rhs.altitude {
295                    Some(a) => Some(-a),
296                    None => None,
297                }
298            },
299        };
300        DMS3d {
301            latitude : self.latitude / rhs.latitude,
302            longitude: self.longitude / rhs.longitude,
303            altitude: altitude,
304        }
305    }
306*/