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*/