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}