1use crate::geoid::{BAND_COUNT, BAND_SIZE, DATA};
2use thiserror::Error;
3
4mod geoid;
5
6#[derive(Error, Debug, PartialEq)]
8pub enum Error {
9 #[error("latitude out of range")]
11 LatitudeOutOfRange,
12 #[error("longitude out of range")]
14 LongitudeOutOfRange,
15}
16
17pub fn geoid_height(latitude: f32, longitude: f32) -> Result<f32, Error> {
35 let latitude = Latitude::new(latitude)?;
36 let longitude = Longitude::new(longitude)?;
37
38 let (band_low, band_high) = latitude.bounding_bands();
39 let (offset_low, offset_high) = longitude.bounding_offsets();
40
41 let h00 = lookup_height(band_low, offset_low);
43 let h01 = lookup_height(band_low, offset_high);
44 let h10 = lookup_height(band_high, offset_low);
45 let h11 = lookup_height(band_high, offset_high);
46
47 let latitude_scalar = latitude.interpolation_scalar();
49 let longitude_scalar = longitude.interpolation_scalar();
50 let latitude_lo = interpolate(h00, h10, latitude_scalar);
51 let latitude_hi = interpolate(h01, h11, latitude_scalar);
52 Ok(interpolate(latitude_lo, latitude_hi, longitude_scalar))
53}
54
55struct Latitude(f32);
57
58impl Latitude {
59 fn new(latitude: f32) -> Result<Self, Error> {
62 match latitude {
63 latitude if (-90.0..=90.0).contains(&latitude) => Ok(Self(latitude)),
64 _ => Err(Error::LatitudeOutOfRange),
65 }
66 }
67
68 fn translate_and_scale(&self) -> f32 {
73 (self.0 + 90.0) / geoid::SCALE
74 }
75
76 fn bounding_bands(&self) -> (usize, usize) {
78 let lat = self.translate_and_scale();
79 let low = lat.floor() as usize;
80 let high = (low + 1).clamp(0, BAND_COUNT - 1);
81 (low, high)
82 }
83
84 fn interpolation_scalar(&self) -> f32 {
86 let lat = self.translate_and_scale();
87 let (lo, hi) = self.bounding_bands();
88 let (lo, hi) = (lo as f32, hi as f32);
89 if lo == hi {
90 return 0.0;
91 }
92 (lat - lo) / (hi - lo)
93 }
94}
95
96struct Longitude(f32);
98
99impl Longitude {
100 fn new(longitude: f32) -> Result<Self, Error> {
103 match longitude {
104 longitude if (-180.0..=180.0).contains(&longitude) => Ok(Self(longitude)),
105 _ => Err(Error::LongitudeOutOfRange),
106 }
107 }
108
109 fn translate_and_scale(&self) -> f32 {
114 (self.0 + 180.0) / geoid::SCALE
115 }
116
117 fn bounding_offsets(&self) -> (usize, usize) {
119 let lon = self.translate_and_scale();
120 let low = lon.floor() as usize;
121 let high = (low + 1).clamp(0, BAND_SIZE - 1);
122 (low, high)
123 }
124
125 fn interpolation_scalar(&self) -> f32 {
127 let lon = self.translate_and_scale();
128 let (lo, hi) = self.bounding_offsets();
129 let (lo, hi) = (lo as f32, hi as f32);
130 if lo == hi {
131 return 0.0;
132 }
133 (lon - lo) / (hi - lo)
134 }
135}
136
137fn lookup_height(band: usize, offset: usize) -> f32 {
145 DATA[band * BAND_SIZE + offset]
146}
147
148fn interpolate(a: f32, b: f32, proportion: f32) -> f32 {
154 a + ((b - a) * proportion)
155}
156
157#[cfg(test)]
158mod tests {
159 use crate::{geoid_height, interpolate, Error};
160
161 #[test]
162 fn test_interpolate() {
163 assert_eq!(interpolate(0.0, 100.0, 0.5), 50.0);
164 assert_eq!(interpolate(100.0, 0.0, 0.5), 50.0);
165 assert_eq!(interpolate(0.0, 0.0, 0.5), 0.0);
166 assert_eq!(interpolate(5.0, 10.0, 0.2), 6.0);
167 }
168
169 #[test]
170 fn fuzz_inputs() {
171 let mut latitude = -90.0;
172 while latitude <= 90.0 {
173 let mut longitude = -180.0;
174 while longitude <= 180.0 {
175 assert!(
176 geoid_height(latitude, longitude).is_ok(),
177 "geoid_height({}, {}) caused a crash",
178 latitude,
179 longitude
180 );
181 longitude += 0.09;
182 }
183 latitude += 0.11;
184 }
185 }
186
187 #[test]
188 fn test_one_invalid_input() {
189 let tests = [
190 (-91.0, 0.0, Error::LatitudeOutOfRange),
191 (91.0, 0.0, Error::LatitudeOutOfRange),
192 (0.0, -181.0, Error::LongitudeOutOfRange),
193 (0.0, 181.0, Error::LongitudeOutOfRange),
194 ];
195 for (lat, lon, expected) in tests {
196 let result = geoid_height(lat, lon);
197 assert!(
198 result.is_err(),
199 "Expected an error computing height for ({}, {})",
200 lat,
201 lon
202 );
203 if let Err(actual) = result {
204 assert_eq!(expected, actual, "Got a different error than expected");
205 }
206 }
207 }
208
209 #[test]
210 fn test_two_invalid_inputs() {
211 assert!(geoid_height(-95.0, -200.0).is_err());
212 }
213
214 #[test]
215 fn validate_known_points() {
216 let tests = [
217 (-90.0, -180.0, -30.15),
218 (-90.0, 0.0, -30.15),
219 (-90.0, 180.0, -30.15),
220 (0.0, -180.0, 21.281),
221 (0.0, 0.0, 17.225),
222 (0.0, 180.0, 21.281),
223 (90.0, -180.0, 14.899),
224 (90.0, 0.0, 14.899),
225 (90.0, 180.0, 14.899),
226 (-81.0, -135.0, -45.184),
227 (72.0, 141.0, -1.603),
228 (-87.0, 49.5, -17.6775),
229 ];
230 for (lat, lon, expected) in tests {
231 let result = geoid_height(lat, lon);
232 assert!(
233 result.is_ok(),
234 "could not calculate geoid height at ({}, {})",
235 lat,
236 lon
237 );
238 if let Ok(actual) = result {
239 assert_eq!(
240 actual, expected,
241 "got unexpected height at ({}, {})",
242 lat, lon
243 );
244 }
245 }
246 }
247}