dms_coordinates/
dms.rs

1//! Angle representation in D°M'S" (sexagesimal format).
2//! Supports arithmetics operation, up to double precision,
3//! for easy navigation calculations.
4use crate::{cardinal::Cardinal, Error};
5
6#[cfg(feature = "serde")]
7use serde_derive::{Deserialize, Serialize};
8
9/// Angle expressed as `D°M'S"`,
10/// in Degrees D°, Minutes M' and fractionnal
11/// Seconds S" (double precision) with an optionnal Cardinal.
12/// When a cardinal is associated to this angle,
13/// we consider this angle represents either a Latitude
14/// or a Longitude angle.
15#[derive(PartialEq, Copy, Clone, Debug)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct DMS {
18    /// Degrees D°
19    pub degrees: u16,
20    /// Minutes M'
21    pub minutes: u8,
22    /// Seconds with fractionnal part S"
23    pub seconds: f64,
24    /// Optionnal cardinal associated to this angle
25    pub cardinal: Option<Cardinal>,
26}
27
28#[derive(Debug, Copy, Clone)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub enum Scale {
31    /// Countries scale is 1°0'0"
32    Country,
33    /// Large cities scale is 0°6'0"
34    LargeCity,
35    /// Cities scale is 0°0'36"
36    City,
37    /// Neighborhood, Strees scale is 0°0'3.6"
38    Neighborhood,
39    /// Single street / large buildings scale is 0°0'0.360"
40    Street,
41    /// Trees / small buildings scale is 0.036"
42    Tree,
43    /// Human / single individual scale is 3.6E-3"
44    Human,
45    /// Roughly precise scale, used in commercial devices, is 360E-6"
46    RoughSurveying,
47    /// Extremely precise scale, used in tectnoic plate mapping for instance, is 36E-6"
48    PreciseSurveying,
49}
50
51impl core::fmt::Display for DMS {
52    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
53        if let Some(cardinal) = self.cardinal {
54            write!(
55                f,
56                "{}°{}'{:.4}\"{}",
57                self.degrees, self.minutes, self.seconds, cardinal,
58            )
59        } else {
60            write!(f, "{}°{}'{:.4}\"", self.degrees, self.minutes, self.seconds)
61        }
62    }
63}
64
65impl Default for DMS {
66    /// Builds null angle with no Cardinal associated to it
67    fn default() -> Self {
68        Self {
69            degrees: 0,
70            minutes: 0,
71            seconds: 0.0_f64,
72            cardinal: None,
73        }
74    }
75}
76
77impl From<DMS> for f64 {
78    /// Converts Self to decimal degrees
79    fn from(val: DMS) -> Self {
80        val.to_ddeg_angle()
81    }
82}
83
84impl From<DMS> for f32 {
85    /// Converts Self into fractionnal seconds with precision loss
86    fn from(val: DMS) -> Self {
87        val.to_ddeg_angle() as f32
88    }
89}
90
91impl From<DMS> for u64 {
92    /// Returns total amount of seconds in Self,
93    /// loosing fractionnal part
94    fn from(val: DMS) -> Self {
95        val.total_seconds().floor() as u64
96    }
97}
98
99impl From<DMS> for u32 {
100    /// Returns total amount of seconds in Self,
101    /// loosing fractionnal part
102    fn from(val: DMS) -> Self {
103        val.total_seconds().floor() as u32
104    }
105}
106
107impl From<DMS> for u16 {
108    /// Returns total amount of seconds in Self,
109    /// loosing fractionnal part
110    fn from(val: DMS) -> Self {
111        val.total_seconds().floor() as u16
112    }
113}
114
115impl From<DMS> for u8 {
116    /// Returns total amount of seconds in Self,
117    /// loosing fractionnal part
118    fn from(val: DMS) -> Self {
119        val.total_seconds().floor() as u8
120    }
121}
122
123impl core::ops::Add<DMS> for DMS {
124    type Output = Result<Self, Error>;
125    fn add(self, rhs: Self) -> Result<Self, Error> {
126        if let Some(c0) = self.cardinal {
127            if let Some(c1) = rhs.cardinal {
128                let a = self.to_ddeg_angle() + rhs.to_ddeg_angle();
129                if c0.is_latitude() && c1.is_latitude() {
130                    Ok(Self::from_ddeg_latitude(a))
131                } else if c0.is_longitude() && c1.is_longitude() {
132                    Ok(Self::from_ddeg_longitude(a))
133                } else {
134                    Err(Error::IncompatibleCardinals)
135                }
136            } else {
137                Ok(Self::from_seconds(
138                    self.total_seconds() + rhs.total_seconds(),
139                ))
140            }
141        } else {
142            Ok(Self::from_seconds(
143                self.total_seconds() + rhs.total_seconds(),
144            ))
145        }
146    }
147}
148
149impl core::ops::AddAssign<DMS> for DMS {
150    fn add_assign(&mut self, rhs: Self) {
151        if let Some(c0) = self.cardinal {
152            if let Some(c1) = rhs.cardinal {
153                let a = self.to_ddeg_angle() + rhs.to_ddeg_angle();
154                if c0.is_latitude() && c1.is_latitude() {
155                    *self = Self::from_ddeg_latitude(a)
156                } else if c0.is_longitude() && c1.is_longitude() {
157                    *self = Self::from_ddeg_longitude(a)
158                }
159            } else {
160                *self = Self::from_seconds(self.total_seconds() + rhs.total_seconds())
161            }
162        } else {
163            *self = Self::from_seconds(self.total_seconds() + rhs.total_seconds())
164        }
165    }
166}
167
168impl core::ops::AddAssign<f64> for DMS {
169    fn add_assign(&mut self, rhs: f64) {
170        if let Some(cardinal) = self.cardinal {
171            let a = self.to_ddeg_angle() + rhs;
172            if cardinal.is_latitude() {
173                *self = Self::from_ddeg_latitude(a)
174            } else {
175                *self = Self::from_ddeg_longitude(a)
176            }
177        } else {
178            *self = Self::from_seconds(self.total_seconds() + rhs)
179        }
180    }
181}
182
183impl core::ops::Add<f64> for DMS {
184    type Output = Self;
185    fn add(self, rhs: f64) -> Self {
186        if let Some(cardinal) = self.cardinal {
187            let a = self.to_ddeg_angle() + rhs;
188            if cardinal.is_latitude() {
189                Self::from_ddeg_latitude(a)
190            } else {
191                Self::from_ddeg_longitude(a)
192            }
193        } else {
194            Self::from_seconds(self.total_seconds() + rhs)
195        }
196    }
197}
198
199impl core::ops::Sub<f64> for DMS {
200    type Output = Self;
201    fn sub(self, rhs: f64) -> Self {
202        if let Some(cardinal) = self.cardinal {
203            let a = self.to_ddeg_angle() - rhs;
204            if cardinal.is_latitude() {
205                Self::from_ddeg_latitude(a)
206            } else {
207                Self::from_ddeg_longitude(a)
208            }
209        } else {
210            Self::from_seconds(self.total_seconds() - rhs)
211        }
212    }
213}
214
215impl core::ops::SubAssign<f64> for DMS {
216    fn sub_assign(&mut self, rhs: f64) {
217        if let Some(cardinal) = self.cardinal {
218            let a = self.to_ddeg_angle() - rhs;
219            if cardinal.is_latitude() {
220                *self = Self::from_ddeg_latitude(a)
221            } else {
222                *self = Self::from_ddeg_longitude(a)
223            }
224        } else {
225            *self = Self::from_seconds(self.total_seconds() - rhs)
226        }
227    }
228}
229
230impl core::ops::Mul<f64> for DMS {
231    type Output = DMS;
232    fn mul(self, rhs: f64) -> DMS {
233        if let Some(cardinal) = self.cardinal {
234            let a = self.to_ddeg_angle() * rhs;
235            if cardinal.is_latitude() {
236                Self::from_ddeg_latitude(a)
237            } else {
238                Self::from_ddeg_longitude(a)
239            }
240        } else {
241            Self::from_seconds(self.total_seconds() * rhs)
242        }
243    }
244}
245
246impl core::ops::Div<f64> for DMS {
247    type Output = DMS;
248    fn div(self, rhs: f64) -> DMS {
249        if let Some(cardinal) = self.cardinal {
250            let a = self.to_ddeg_angle() / rhs;
251            if cardinal.is_latitude() {
252                Self::from_ddeg_latitude(a)
253            } else {
254                Self::from_ddeg_longitude(a)
255            }
256        } else {
257            Self::from_seconds(self.total_seconds() / rhs)
258        }
259    }
260}
261
262impl core::ops::MulAssign<f64> for DMS {
263    fn mul_assign(&mut self, rhs: f64) {
264        if let Some(cardinal) = self.cardinal {
265            let a = self.to_ddeg_angle() * rhs;
266            if cardinal.is_latitude() {
267                *self = Self::from_ddeg_latitude(a)
268            } else {
269                *self = Self::from_ddeg_longitude(a)
270            }
271        } else {
272            *self = Self::from_seconds(self.total_seconds() * rhs)
273        }
274    }
275}
276
277impl core::ops::DivAssign<f64> for DMS {
278    fn div_assign(&mut self, rhs: f64) {
279        if let Some(cardinal) = self.cardinal {
280            let a = self.to_ddeg_angle() / rhs;
281            if cardinal.is_latitude() {
282                *self = Self::from_ddeg_latitude(a)
283            } else {
284                *self = Self::from_ddeg_longitude(a)
285            }
286        } else {
287            *self = Self::from_seconds(self.total_seconds() / rhs)
288        }
289    }
290}
291
292impl DMS {
293    /// Builds `D°M'S"` angle, from given D°, M', S" values.
294    /// This method allows overflow, it will wrapp values to correct range
295    /// itself.
296    pub fn new(degrees: u16, minutes: u8, seconds: f64, cardinal: Option<Cardinal>) -> DMS {
297        let d = Self::from_seconds(degrees as f64 * 3600.0 + minutes as f64 * 60.0 + seconds);
298        if let Some(cardinal) = cardinal {
299            d.with_cardinal(cardinal)
300        } else {
301            d
302        }
303    }
304
305    /// Builds `D°M'S"` angle from total amount of seconds
306    pub fn from_seconds(seconds: f64) -> Self {
307        let degrees = (seconds / 3600.0).floor();
308        let minutes = ((seconds - degrees * 3600.0) / 60.0).floor();
309        let integer = ((seconds - degrees * 3600.0 - minutes * 60.0).floor() as u8) % 60;
310        Self {
311            degrees: (degrees as u16) % 360,
312            minutes: minutes as u8,
313            seconds: integer as f64 + seconds.fract(),
314            cardinal: None,
315        }
316    }
317
318    /// Returns same D°M'S" angle but attaches a cardinal to it.
319    /// Useful to convert make this D°M'S" angle a Latitude or a
320    /// Longitude.
321    pub fn with_cardinal(&self, cardinal: Cardinal) -> Self {
322        Self {
323            degrees: self.degrees,
324            minutes: self.minutes,
325            seconds: self.seconds,
326            cardinal: Some(cardinal),
327        }
328    }
329
330    /// Builds D°M'S" angle from given angle expressed in
331    /// decimal degrees, with no cardinal associated to returned value
332    pub fn from_ddeg_angle(angle: f64) -> Self {
333        let degrees = angle.abs().floor();
334        let minutes = ((angle.abs() - degrees) * 60.0).floor();
335        let seconds = (angle.abs() - degrees - minutes / 60.0_f64) * 3600.0_f64;
336        Self {
337            degrees: degrees as u16,
338            minutes: minutes as u8,
339            seconds,
340            cardinal: None,
341        }
342    }
343
344    /// Builds Latitude angle, expressed in D°M'S", from
345    /// given angle expressed in decimal degrees
346    pub fn from_ddeg_latitude(angle: f64) -> Self {
347        let degrees = angle.abs().floor();
348        let minutes = ((angle.abs() - degrees) * 60.0).floor();
349        let seconds = (angle.abs() - degrees - minutes / 60.0_f64) * 3600.0_f64;
350        let cardinal = if angle < 0.0 {
351            Cardinal::South
352        } else {
353            Cardinal::North
354        };
355        Self {
356            degrees: (degrees as u16) % 90,
357            minutes: minutes as u8,
358            seconds,
359            cardinal: Some(cardinal),
360        }
361    }
362
363    /// Builds Longitude angle, expressed in D°M'S",
364    /// from given angle expressed in decimal degrees
365    pub fn from_ddeg_longitude(angle: f64) -> Self {
366        let degrees = angle.abs().floor();
367        let minutes = ((angle.abs() - degrees) * 60.0).floor();
368        let seconds = (angle.abs() - degrees - minutes / 60.0_f64) * 3600.0_f64;
369        let cardinal = if angle < 0.0 {
370            Cardinal::West
371        } else {
372            Cardinal::East
373        };
374        Self {
375            degrees: (degrees as u16) % 180,
376            minutes: minutes as u8,
377            seconds,
378            cardinal: Some(cardinal),
379        }
380    }
381
382    /// Returns Self expressed in decimal degrees
383    /// If no cardinal is associated, returned angle strictly > 0.
384    pub fn to_ddeg_angle(&self) -> f64 {
385        let d = self.degrees as f64 + self.minutes as f64 / 60.0_f64 + self.seconds / 3600.0_f64;
386        match self.cardinal {
387            Some(cardinal) => {
388                if cardinal.is_southern() || cardinal.is_western() {
389                    -d
390                } else {
391                    d
392                }
393            }
394            None => d,
395        }
396    }
397
398    /// Adds given angle to Self, angle expressed a decimal degrees
399    pub fn add_ddeg(&mut self, angle: f64) {
400        *self = Self::from_ddeg_angle(self.to_ddeg_angle() + angle);
401    }
402
403    /// Returns copy of Self with given angle added, as decimal degrees
404    pub fn with_ddeg_angle(&self, angle: f64) -> Self {
405        Self::from_ddeg_angle(self.to_ddeg_angle() + angle)
406    }
407
408    /// Returns total of seconds (base unit) contained in Self
409    pub fn total_seconds(&self) -> f64 {
410        self.degrees as f64 * 3600.0 + self.minutes as f64 * 60.0 + self.seconds
411    }
412
413    /// Converts self to radians
414    pub fn to_radians(&self) -> f64 {
415        self.to_ddeg_angle() / 180.0 * core::f64::consts::PI
416    }
417    /*
418        /// Descriptor must follow standard formats:
419        ///     +DDD.D  : sign + 3 digit "." + 1digit
420        ///     Degrees specified, minutes = 0, seconds = 0
421        ///     +DDDMM.M : sign + 3 digit D° + 2 digit M' "." 1 digit M'
422        ///     Degrees + minutes specified
423        ///     +DDDMMSS.S : sign + 3 digit D° + 2 digit M' + fractionnal seconds
424        /// <!> Although standards says "+" is mandatory to describe positive D°,
425        /// this method tolerates a missing '+' and we interprate D° as positive value.
426        pub fn from_str (s: &str) -> Result<Self, ParseError> {
427            let lon_positive_d = Regex::new(r"^+\d{3}.\d{1}$")
428                .unwrap();
429            let lon_negative_d = Regex::new(r"^-\d{3}.\d{1}$")
430                .unwrap();
431            let lon_positive_dm = Regex::new(r"^+\d{3}d{2}.\d{1}$")
432                .unwrap();
433            let lon_negative_dm = Regex::new(r"^-\d{d}d{2}.\d{1}$")
434                .unwrap();
435            let lon_positive_dms = Regex::new(r"^+\d{3}d{2}d{2}.\d{1}$")
436                .unwrap();
437            let lon_negative_dms = Regex::new(r"^-\d{3}d{2}d{2}.\d{1}$")
438                .unwrap();
439            if lon_positive_d.is_match(s) {
440                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
441                Ok(DMS {
442                    degrees,
443                    minutes: 0,
444                    seconds: 0.0,
445                    cardinal: Some(Cardinal::East),
446                })
447            } else if lon_negative_d.is_match(s) {
448                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
449                Ok(DMS {
450                    degrees,
451                    minutes: 0,
452                    seconds: 0.0,
453                    cardinal: Some(Cardinal::West),
454                })
455
456            } else if lon_positive_dm.is_match(s) {
457                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
458                Ok(DMS {
459                    degrees,
460                    minutes: 0,
461                    seconds: 0.0,
462                    cardinal: Some(Cardinal::East),
463                })
464
465            } else if lon_negative_dm.is_match(s) {
466                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
467                Ok(DMS {
468                    degrees,
469                    minutes: 0,
470                    seconds: 0.0,
471                    cardinal: Some(Cardinal::West),
472                })
473
474            } else if lon_positive_dms.is_match(s) {
475                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
476                Ok(DMS {
477                    degrees,
478                    minutes: 0,
479                    seconds: 0.0,
480                    cardinal: Some(Cardinal::East),
481                })
482
483            } else if lon_negative_dms.is_match(s) {
484                let degrees = u16::from_str_radix(&s[0..3], 10)?; //attention au '+'
485                Ok(DMS {
486                    degrees,
487                    minutes: 0,
488                    seconds: 0.0,
489                    cardinal: Some(Cardinal::West),
490                })
491
492            } else {
493                Err(ParseError::FormatNotRecognized)
494            }
495        }
496    */
497
498    /// Returns D°M'S" angle copy with
499    /// WGS84 to EU50 conversion applied.
500    /// For conversion to be applied, we need a cardinal to be associated,
501    /// otherwise this simply returns a copy
502    pub fn to_europe50(&self) -> Result<DMS, Error> {
503        if let Some(cardinal) = self.cardinal {
504            if cardinal.is_latitude() {
505                *self + DMS::new(0, 0, 3.6, Some(Cardinal::North))
506            } else {
507                *self + DMS::new(0, 0, 2.4, Some(Cardinal::East))
508            }
509        } else {
510            Ok(*self)
511        }
512    }
513}