jord/
angle.rs

1use crate::{impl_measurement, Measurement};
2use std::f64::consts::PI;
3
4#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Default)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] // codecov:ignore:this
6/// A one-dimensional angle.
7///
8/// It primarely exists to unambigously represent an angle as opposed to a bare
9/// [f64] (which could be anything and in any unit).
10/// It allows conversion to or from radians and degrees.
11///
12/// [Angle] implements many traits, including [Add](::std::ops::Add), [Sub](::std::ops::Sub),
13/// [Mul](::std::ops::Mul) and [Div](::std::ops::Div), among others.
14pub struct Angle {
15    radians: f64,
16}
17
18impl Angle {
19    /// Zero angle.
20    pub const ZERO: Angle = Angle { radians: 0.0 };
21
22    /// 90 degrees angle.
23    pub const QUARTER_CIRCLE: Angle = Angle { radians: PI / 2.0 };
24
25    /// -90 degrees angle.
26    pub const NEG_QUARTER_CIRCLE: Angle = Angle { radians: -PI / 2.0 };
27
28    /// 180 degrees angle.
29    pub const HALF_CIRCLE: Angle = Angle { radians: PI };
30
31    /// -180 degrees angle.
32    pub const NEG_HALF_CIRCLE: Angle = Angle { radians: -PI };
33
34    /// 360 degrees angle.
35    pub const FULL_CIRCLE: Angle = Angle { radians: 2.0 * PI };
36
37    /// `f64::EPSILON` radians.
38    pub(crate) const DBL_EPSILON: Angle = Angle {
39        radians: f64::EPSILON,
40    };
41
42    /// Converts this angle to a floating point value in degrees.
43    pub fn as_degrees(&self) -> f64 {
44        self.radians.to_degrees()
45    }
46
47    /// Converts this angle to a floating point value in radians.
48    #[inline]
49    pub fn as_radians(&self) -> f64 {
50        self.radians
51    }
52
53    /// Creates an angle from a floating point value in degrees.
54    pub fn from_degrees(degrees: f64) -> Self {
55        Angle {
56            radians: degrees.to_radians(),
57        }
58    }
59
60    /// Creates an angle from a floating point value in radians.
61    pub const fn from_radians(radians: f64) -> Self {
62        Angle { radians }
63    }
64
65    /// Returns a new angle that is the absolute value of this angle.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use jord::Angle;
71    ///
72    /// assert_eq!(Angle::from_degrees(45.0), Angle::from_degrees(-45.0).abs());
73    /// ```
74    pub fn abs(&self) -> Self {
75        Angle {
76            radians: self.radians.abs(),
77        }
78    }
79
80    /// Returns a new angle by normalising this angle to the range [0, 360) degrees.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use jord::Angle;
86    ///
87    /// assert_eq!(Angle::from_degrees(359.0), Angle::from_degrees(-361.0).normalised());
88    /// assert_eq!(Angle::from_degrees(358.0), Angle::from_degrees(-2.0).normalised());
89    /// assert_eq!(Angle::from_degrees(154.0), Angle::from_degrees(154.0).normalised());
90    /// assert_eq!(Angle::ZERO, Angle::from_degrees(360.0).normalised());
91    /// ```
92    pub fn normalised(&self) -> Self {
93        self.normalised_to(Self::FULL_CIRCLE)
94    }
95
96    /// Returns a new angle by normalising this angle to the range [0, `max`) degrees.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use jord::Angle;
102    ///
103    /// assert_eq!(
104    ///     Angle::from_degrees(179.0),
105    ///     Angle::from_degrees(-181.0).normalised_to(Angle::HALF_CIRCLE).round_d7()
106    /// );
107    /// assert_eq!(
108    ///     Angle::from_degrees(1.0),
109    ///     Angle::from_degrees(181.0).normalised_to(Angle::HALF_CIRCLE).round_d7()
110    /// );
111    /// assert_eq!(
112    ///     Angle::from_degrees(154.0),
113    ///     Angle::from_degrees(154.0).normalised_to(Angle::HALF_CIRCLE)
114    /// );
115    /// assert_eq!(
116    ///     Angle::ZERO,
117    ///     Angle::from_degrees(180.0).normalised_to(Angle::HALF_CIRCLE)
118    /// );
119    /// ```
120    pub fn normalised_to(&self, max: Angle) -> Angle {
121        if self.radians >= 0.0 && self.radians < max.radians {
122            *self
123        } else {
124            let res = self.radians % max.radians;
125            if res < 0.0 {
126                Self::from_radians(res + max.radians)
127            } else {
128                Self::from_radians(res)
129            }
130        }
131    }
132
133    /// Rounds this angle to the nearest decimal degrees with 5 decimal places - when representing
134    /// an Earth latitude/longtiude this is approximately 1.11 metres at the equator.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use jord::Angle;
140    ///
141    /// assert_eq!(Angle::from_degrees(3.44444), Angle::from_degrees(3.444444).round_d5());
142    /// assert_eq!(Angle::from_degrees(3.44445), Angle::from_degrees(3.444445).round_d5());
143    /// ```
144    pub fn round_d5(&self) -> Self {
145        let d5 = (self.as_degrees() * 1e5).round() / 1e5;
146        Self::from_degrees(d5)
147    }
148
149    /// Rounds this angle to the nearest decimal degrees with 6 decimal places - when representing
150    /// an Earth latitude/longtiude this is approximately 111 millimetres at the equator.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use jord::Angle;
156    ///
157    /// assert_eq!(Angle::from_degrees(3.444444), Angle::from_degrees(3.4444444).round_d6());
158    /// assert_eq!(Angle::from_degrees(3.444445), Angle::from_degrees(3.4444445).round_d6());
159    /// ```
160    pub fn round_d6(&self) -> Self {
161        let d6 = (self.as_degrees() * 1e6).round() / 1e6;
162        Self::from_degrees(d6)
163    }
164
165    /// Rounds this angle to the nearest decimal degrees with 7 decimal places - when representing
166    /// an Earth latitude/longtiude this is approximately 11.1 millimetres at the equator.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use jord::Angle;
172    ///
173    /// assert_eq!(Angle::from_degrees(3.4444444), Angle::from_degrees(3.44444444).round_d7());
174    /// assert_eq!(Angle::from_degrees(3.4444445), Angle::from_degrees(3.44444445).round_d7());
175    /// ```
176    pub fn round_d7(&self) -> Self {
177        let d7 = (self.as_degrees() * 1e7).round() / 1e7;
178        Self::from_degrees(d7)
179    }
180}
181
182impl Measurement for Angle {
183    fn from_default_unit(amount: f64) -> Self {
184        Angle::from_radians(amount)
185    }
186
187    #[inline]
188    fn as_default_unit(&self) -> f64 {
189        self.as_radians()
190    }
191}
192
193impl_measurement! { Angle }
194
195#[cfg(test)]
196mod tests {
197
198    use std::f64::consts::PI;
199
200    use crate::Angle;
201
202    #[test]
203    fn conversions() {
204        assert_eq!(PI, Angle::from_degrees(180.0).as_radians());
205        assert_eq!(180.0, Angle::from_radians(PI).as_degrees());
206    }
207
208    #[test]
209    fn std_ops() {
210        assert_eq!(Angle::from_degrees(2.0), 2.0 * Angle::from_degrees(1.0));
211        assert_eq!(
212            Angle::from_degrees(2.0),
213            Angle::from_degrees(1.0) + Angle::from_degrees(1.0)
214        );
215        assert_eq!(
216            Angle::from_degrees(0.0),
217            Angle::from_degrees(1.0) - Angle::from_degrees(1.0)
218        );
219    }
220
221    #[test]
222    fn normalised() {
223        assert_eq!(
224            Angle::from_degrees(359.0),
225            Angle::from_degrees(-361.0).normalised()
226        );
227        assert_eq!(
228            Angle::from_degrees(358.0),
229            Angle::from_degrees(-2.0).normalised()
230        );
231        assert_eq!(
232            Angle::from_degrees(154.0),
233            Angle::from_degrees(154.0).normalised()
234        );
235        assert_eq!(Angle::ZERO, Angle::from_degrees(360.0).normalised());
236    }
237
238    #[test]
239    fn normalised_to() {
240        assert_eq!(
241            Angle::from_degrees(179.0),
242            Angle::from_degrees(-181.0)
243                .normalised_to(Angle::HALF_CIRCLE)
244                .round_d7()
245        );
246        assert_eq!(
247            Angle::from_degrees(1.0),
248            Angle::from_degrees(181.0)
249                .normalised_to(Angle::HALF_CIRCLE)
250                .round_d7()
251        );
252        assert_eq!(
253            Angle::from_degrees(154.0),
254            Angle::from_degrees(154.0).normalised_to(Angle::HALF_CIRCLE)
255        );
256        assert_eq!(
257            Angle::ZERO,
258            Angle::from_degrees(180.0).normalised_to(Angle::HALF_CIRCLE)
259        );
260    }
261}