1const FEET_TO_METERS: f64 = 0.3048;
2
3#[derive(Clone, Copy, Debug, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
7pub struct EastNorthElevation {
8 pub easting: f64,
9 pub northing: f64,
10 pub up: f64,
11}
12
13impl EastNorthElevation {
14 #[must_use]
15 pub fn from_meters(easting: f64, northing: f64, up: f64) -> Self {
16 Self {
17 easting,
18 northing,
19 up,
20 }
21 }
22
23 #[must_use]
24 pub fn from_feet(easting: f64, northing: f64, up: f64) -> Self {
25 Self {
26 easting: easting * FEET_TO_METERS,
27 northing: northing * FEET_TO_METERS,
28 up: up * FEET_TO_METERS,
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
34#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
35pub struct UtmLocation {
36 pub east_north_elevation: EastNorthElevation,
37 pub zone: u8,
38 pub convergence_angle: f64,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
42#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
43pub struct Date {
44 pub month: u8,
45 pub day: u8,
46 pub year: u16,
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use float_eq::assert_float_eq;
53
54 #[test]
56 fn test_from_meters_basic() {
57 let ene = EastNorthElevation::from_meters(100.0, 200.0, 300.0);
58 assert_float_eq!(ene.easting, 100.0, abs <= 1e-10);
59 assert_float_eq!(ene.northing, 200.0, abs <= 1e-10);
60 assert_float_eq!(ene.up, 300.0, abs <= 1e-10);
61 }
62
63 #[test]
64 fn test_from_meters_zero() {
65 let ene = EastNorthElevation::from_meters(0.0, 0.0, 0.0);
66 assert_float_eq!(ene.easting, 0.0, abs <= 1e-10);
67 assert_float_eq!(ene.northing, 0.0, abs <= 1e-10);
68 assert_float_eq!(ene.up, 0.0, abs <= 1e-10);
69 }
70
71 #[test]
72 fn test_from_meters_negative() {
73 let ene = EastNorthElevation::from_meters(-100.0, -200.0, -50.0);
74 assert_float_eq!(ene.easting, -100.0, abs <= 1e-10);
75 assert_float_eq!(ene.northing, -200.0, abs <= 1e-10);
76 assert_float_eq!(ene.up, -50.0, abs <= 1e-10);
77 }
78
79 #[test]
80 fn test_from_feet_basic() {
81 let ene = EastNorthElevation::from_feet(100.0, 200.0, 300.0);
83 assert_float_eq!(ene.easting, 30.48, abs <= 1e-10);
84 assert_float_eq!(ene.northing, 60.96, abs <= 1e-10);
85 assert_float_eq!(ene.up, 91.44, abs <= 1e-10);
86 }
87
88 #[test]
89 fn test_from_feet_zero() {
90 let ene = EastNorthElevation::from_feet(0.0, 0.0, 0.0);
91 assert_float_eq!(ene.easting, 0.0, abs <= 1e-10);
92 assert_float_eq!(ene.northing, 0.0, abs <= 1e-10);
93 assert_float_eq!(ene.up, 0.0, abs <= 1e-10);
94 }
95
96 #[test]
97 fn test_from_feet_negative() {
98 let ene = EastNorthElevation::from_feet(-10.0, -20.0, -30.0);
99 assert_float_eq!(ene.easting, -3.048, abs <= 1e-10);
100 assert_float_eq!(ene.northing, -6.096, abs <= 1e-10);
101 assert_float_eq!(ene.up, -9.144, abs <= 1e-10);
102 }
103
104 #[test]
105 fn test_from_feet_known_conversion() {
106 let ene = EastNorthElevation::from_feet(1.0, 1.0, 1.0);
108 assert_float_eq!(ene.easting, FEET_TO_METERS, abs <= 1e-10);
109 assert_float_eq!(ene.northing, FEET_TO_METERS, abs <= 1e-10);
110 assert_float_eq!(ene.up, FEET_TO_METERS, abs <= 1e-10);
111 }
112
113 #[test]
114 fn test_from_feet_large_values() {
115 let ene = EastNorthElevation::from_feet(1_000_000.0, 4_000_000.0, 10_000.0);
117 assert_float_eq!(ene.easting, 304_800.0, abs <= 1e-6);
118 assert_float_eq!(ene.northing, 1_219_200.0, abs <= 1e-6);
119 assert_float_eq!(ene.up, 3_048.0, abs <= 1e-6);
120 }
121
122 #[test]
124 fn test_utm_location_creation() {
125 let ene = EastNorthElevation::from_meters(500_000.0, 4_500_000.0, 1500.0);
126 let utm = UtmLocation {
127 east_north_elevation: ene,
128 zone: 13,
129 convergence_angle: -0.5,
130 };
131 assert_eq!(utm.zone, 13);
132 assert_float_eq!(utm.convergence_angle, -0.5, abs <= 1e-10);
133 assert_float_eq!(utm.east_north_elevation.easting, 500_000.0, abs <= 1e-10);
134 }
135
136 #[test]
137 fn test_utm_location_zone_boundaries() {
138 let ene = EastNorthElevation::from_meters(0.0, 0.0, 0.0);
139
140 let utm1 = UtmLocation {
142 east_north_elevation: ene,
143 zone: 1,
144 convergence_angle: 0.0,
145 };
146 assert_eq!(utm1.zone, 1);
147
148 let utm60 = UtmLocation {
150 east_north_elevation: ene,
151 zone: 60,
152 convergence_angle: 0.0,
153 };
154 assert_eq!(utm60.zone, 60);
155 }
156
157 #[test]
158 fn test_utm_location_convergence_angle_range() {
159 let ene = EastNorthElevation::from_meters(500_000.0, 4_500_000.0, 0.0);
160
161 let utm_pos = UtmLocation {
163 east_north_elevation: ene,
164 zone: 13,
165 convergence_angle: 2.5,
166 };
167 assert_float_eq!(utm_pos.convergence_angle, 2.5, abs <= 1e-10);
168
169 let utm_neg = UtmLocation {
171 east_north_elevation: ene,
172 zone: 13,
173 convergence_angle: -2.5,
174 };
175 assert_float_eq!(utm_neg.convergence_angle, -2.5, abs <= 1e-10);
176 }
177
178 #[test]
180 fn test_date_creation() {
181 let date = Date {
182 month: 6,
183 day: 15,
184 year: 2024,
185 };
186 assert_eq!(date.month, 6);
187 assert_eq!(date.day, 15);
188 assert_eq!(date.year, 2024);
189 }
190
191 #[test]
192 fn test_date_january_first() {
193 let date = Date {
194 month: 1,
195 day: 1,
196 year: 2000,
197 };
198 assert_eq!(date.month, 1);
199 assert_eq!(date.day, 1);
200 assert_eq!(date.year, 2000);
201 }
202
203 #[test]
204 fn test_date_december_last() {
205 let date = Date {
206 month: 12,
207 day: 31,
208 year: 1999,
209 };
210 assert_eq!(date.month, 12);
211 assert_eq!(date.day, 31);
212 assert_eq!(date.year, 1999);
213 }
214
215 #[test]
216 fn test_date_leap_year() {
217 let date = Date {
218 month: 2,
219 day: 29,
220 year: 2024,
221 };
222 assert_eq!(date.month, 2);
223 assert_eq!(date.day, 29);
224 assert_eq!(date.year, 2024);
225 }
226}