Skip to main content

geodesy/math/
angular.rs

1use log::warn;
2
3/// Simplistic transformation from degrees, minutes and seconds-with-decimals
4/// to degrees-with-decimals. No sanity check: Sign taken from degree-component,
5/// minutes forced to unsigned by i16 type, but passing a negative value for
6/// seconds leads to undefined behaviour.
7pub fn dms_to_dd(d: i32, m: u16, s: f64) -> f64 {
8    (d.abs() as f64 + (m as f64 + s / 60.) / 60.).copysign(d as f64)
9}
10
11/// Simplistic transformation from degrees and minutes-with-decimals
12/// to degrees-with-decimals. No sanity check: Sign taken from
13/// degree-component, but passing a negative value for minutes leads
14/// to undefined behaviour.
15pub fn dm_to_dd(d: i32, m: f64) -> f64 {
16    (d.abs() as f64 + (m / 60.)).copysign(d as f64)
17}
18
19/// Simplistic transformation from the ISO-6709 DDDMM.mmm format to
20/// to degrees-with-decimals. No sanity check: Invalid input,
21/// such as 5575.75 (where the number of minutes exceed 60) leads
22/// to undefined behaviour.
23pub fn iso_dm_to_dd(iso_dm: f64) -> f64 {
24    let magn = iso_dm.abs();
25    let dm = magn.trunc() as u64;
26    let d = (dm / 100) as f64;
27    let m = (dm % 100) as f64 + magn.fract();
28    (d + (m / 60.)).copysign(iso_dm)
29}
30
31/// Transformation from degrees-with-decimals to the ISO-6709 DDDMM.mmm format.
32pub fn dd_to_iso_dm(dd: f64) -> f64 {
33    let dm = dd.abs();
34    let d = dm.trunc();
35    let m = dm.fract() * 60.;
36    (d * 100. + m).copysign(dd)
37}
38
39/// Simplistic transformation from the ISO-6709 DDDMMSS.sss
40/// format to degrees-with-decimals. No sanity check: Invalid input,
41/// such as 557575.75 (where the number of minutes and seconds both
42/// exceed 60) leads to undefined behaviour.
43pub fn iso_dms_to_dd(iso_dms: f64) -> f64 {
44    let magn = iso_dms.abs();
45    let dms = magn as u32;
46    let d = dms / 10000;
47    let ms = dms % 10000;
48    let m = ms / 100;
49    let s = (ms % 100) as f64 + magn.fract();
50    (d as f64 + ((s / 60.) + m as f64) / 60.).copysign(iso_dms)
51}
52
53/// Transformation from degrees-with-decimals to the extended
54/// ISO-6709 DDDMMSS.sss format.
55pub fn dd_to_iso_dms(dd: f64) -> f64 {
56    let magn = dd.abs();
57    let d = magn.trunc();
58    let mm = magn.fract() * 60.;
59    let m = mm.trunc();
60    let s = mm.fract() * 60.;
61    (d * 10000. + m * 100. + s).copysign(dd)
62}
63
64/// normalize arbitrary angles to [-π, π)
65pub fn normalize_symmetric(angle: f64) -> f64 {
66    use std::f64::consts::PI;
67    let angle = (angle + PI) % (2.0 * PI);
68    angle - PI * angle.signum()
69}
70
71/// normalize arbitrary angles to [0, 2π)
72pub fn normalize_positive(angle: f64) -> f64 {
73    use std::f64::consts::PI;
74    let angle = angle % (2.0 * PI);
75    if angle < 0. {
76        return angle + 2.0 * PI;
77    }
78    angle
79}
80
81/// Parse sexagesimal degrees, i.e. degrees, minutes and seconds in the
82/// format 45:30:36, 45:30:36N,-45:30:36 etc.
83pub fn parse_sexagesimal(angle: &str) -> f64 {
84    // Degrees, minutes, and seconds
85    let mut dms = [0.0, 0.0, 0.0];
86    let mut angle = angle.trim();
87
88    // Empty?
89    let n = angle.len();
90    if n == 0 || angle == "NaN" {
91        return f64::NAN;
92    }
93
94    // Handle NSEW indicators
95    let mut postfix_sign = 1.0;
96    if "wWsSeEnN".contains(&angle[n - 1..]) {
97        if "wWsS".contains(&angle[n - 1..]) {
98            postfix_sign = -1.0;
99        }
100        angle = &angle[..n - 1];
101    }
102
103    // Split into as many elements as given: D, D:M, D:M:S
104    for (i, element) in angle.split(':').enumerate() {
105        if i < 3 {
106            if let Ok(v) = element.parse::<f64>() {
107                dms[i] = v;
108                continue;
109            }
110        }
111        // More than 3 elements?
112        warn!("Cannot parse {angle} as a real number or sexagesimal angle");
113        return f64::NAN;
114    }
115
116    // Sexagesimal conversion if we have more than one element. Otherwise
117    // decay gracefully to plain real/f64 conversion
118    let sign = dms[0].signum() * postfix_sign;
119    sign * (dms[0].abs() + (dms[1] + dms[2] / 60.0) / 60.0)
120}
121
122// ----- Tests ---------------------------------------------------------------------
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_angular() {
130        // dms
131        assert_eq!(dms_to_dd(55, 30, 36.), 55.51);
132        assert_eq!(dm_to_dd(55, 30.60), 55.51);
133
134        // iso_dm + iso_dms
135        assert!((iso_dm_to_dd(5530.60) - 55.51).abs() < 1e-10);
136        assert!((iso_dm_to_dd(15530.60) - 155.51).abs() < 1e-10);
137        assert!((iso_dm_to_dd(-15530.60) + 155.51).abs() < 1e-10);
138        assert!((iso_dms_to_dd(553036.0) - 55.51).abs() < 1e-10);
139        assert_eq!(dd_to_iso_dm(55.5025), 5530.15);
140        assert_eq!(dd_to_iso_dm(-55.5025), -5530.15);
141        assert_eq!(dd_to_iso_dms(55.5025), 553009.);
142        assert_eq!(dd_to_iso_dms(-55.51), -553036.);
143
144        assert_eq!(iso_dm_to_dd(5500.), 55.);
145        assert_eq!(iso_dm_to_dd(-5500.), -55.);
146        assert_eq!(iso_dm_to_dd(5530.60), -iso_dm_to_dd(-5530.60));
147        assert_eq!(iso_dms_to_dd(553036.), -iso_dms_to_dd(-553036.00));
148    }
149
150    #[test]
151    fn test_parse_sexagesimal() {
152        assert_eq!(1.51, parse_sexagesimal("1:30:36"));
153        assert_eq!(-1.51, parse_sexagesimal("-1:30:36"));
154        assert_eq!(1.51, parse_sexagesimal("1:30:36N"));
155        assert_eq!(-1.51, parse_sexagesimal("1:30:36S"));
156        assert_eq!(1.51, parse_sexagesimal("1:30:36e"));
157        assert_eq!(-1.51, parse_sexagesimal("1:30:36w"));
158        assert!(parse_sexagesimal("q1:30:36w").is_nan());
159    }
160}