leodos_protocols/network/isl/
geo.rs1use core::f32::consts::PI;
17
18const EARTH_RADIUS_M: f32 = 6_371_000.0;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct LatLon {
23 pub lat_deg: f32,
25 pub lon_deg: f32,
27}
28
29impl LatLon {
30 pub fn new(lat_deg: f32, lon_deg: f32) -> Self {
32 Self { lat_deg, lon_deg }
33 }
34
35 pub fn lat_rad(&self) -> f32 {
37 self.lat_deg * PI / 180.0
38 }
39
40 pub fn lon_rad(&self) -> f32 {
42 self.lon_deg * PI / 180.0
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct Ecef {
49 pub x: f32,
51 pub y: f32,
53 pub z: f32,
55}
56
57impl Ecef {
58 pub fn from_latlon(coord: LatLon) -> Self {
60 let phi = coord.lat_rad();
61 let lambda = coord.lon_rad();
62 let cos_phi = libm::cosf(phi);
63
64 Self {
65 x: EARTH_RADIUS_M * cos_phi * libm::cosf(lambda),
66 y: EARTH_RADIUS_M * cos_phi * libm::sinf(lambda),
67 z: EARTH_RADIUS_M * libm::sinf(phi),
68 }
69 }
70
71 pub fn to_latlon(&self) -> LatLon {
73 let r = libm::sqrtf(self.x * self.x + self.y * self.y + self.z * self.z);
74 let lat_rad = libm::asinf(self.z / r);
75 let lon_rad = libm::atan2f(self.y, self.x);
76
77 LatLon {
78 lat_deg: lat_rad * 180.0 / PI,
79 lon_deg: lon_rad * 180.0 / PI,
80 }
81 }
82
83 pub fn distance(&self, other: &Ecef) -> f32 {
85 let dx = self.x - other.x;
86 let dy = self.y - other.y;
87 let dz = self.z - other.z;
88 libm::sqrtf(dx * dx + dy * dy + dz * dz)
89 }
90
91 pub fn elevation_from(&self, station: LatLon) -> f32 {
98 let gs = Ecef::from_latlon(station);
99 let dx = self.x - gs.x;
100 let dy = self.y - gs.y;
101 let dz = self.z - gs.z;
102 let dist = libm::sqrtf(dx * dx + dy * dy + dz * dz);
103 if dist < 1.0 {
104 return 90.0;
105 }
106
107 let gs_mag = libm::sqrtf(
109 gs.x * gs.x + gs.y * gs.y + gs.z * gs.z,
110 );
111 let ux = gs.x / gs_mag;
112 let uy = gs.y / gs_mag;
113 let uz = gs.z / gs_mag;
114
115 let sin_el = (dx * ux + dy * uy + dz * uz) / dist;
117 libm::asinf(sin_el) * 180.0 / PI
118 }
119}
120
121#[derive(Debug, Clone, Copy)]
123pub struct GeoAoi {
124 pub upper_left: LatLon,
126 pub lower_right: LatLon,
128}
129
130impl GeoAoi {
131 pub fn new(upper_left: LatLon, lower_right: LatLon) -> Self {
133 Self {
134 upper_left,
135 lower_right,
136 }
137 }
138
139 pub fn contains(&self, point: LatLon) -> bool {
141 let lat_ok =
142 point.lat_deg <= self.upper_left.lat_deg && point.lat_deg >= self.lower_right.lat_deg;
143 let lon_ok =
144 point.lon_deg >= self.upper_left.lon_deg && point.lon_deg <= self.lower_right.lon_deg;
145 lat_ok && lon_ok
146 }
147
148 pub fn center(&self) -> LatLon {
150 LatLon {
151 lat_deg: (self.upper_left.lat_deg + self.lower_right.lat_deg) / 2.0,
152 lon_deg: (self.upper_left.lon_deg + self.lower_right.lon_deg) / 2.0,
153 }
154 }
155
156 pub fn width_deg(&self) -> f32 {
158 self.lower_right.lon_deg - self.upper_left.lon_deg
159 }
160
161 pub fn height_deg(&self) -> f32 {
163 self.upper_left.lat_deg - self.lower_right.lat_deg
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
172 (a - b).abs() < epsilon
173 }
174
175 #[test]
176 fn test_ecef_equator_prime_meridian() {
177 let coord = LatLon::new(0.0, 0.0);
178 let ecef = Ecef::from_latlon(coord);
179
180 assert!(approx_eq(ecef.x, EARTH_RADIUS_M, 1.0));
181 assert!(approx_eq(ecef.y, 0.0, 1.0));
182 assert!(approx_eq(ecef.z, 0.0, 1.0));
183 }
184
185 #[test]
186 fn test_ecef_north_pole() {
187 let coord = LatLon::new(90.0, 0.0);
188 let ecef = Ecef::from_latlon(coord);
189
190 assert!(approx_eq(ecef.x, 0.0, 1.0));
191 assert!(approx_eq(ecef.y, 0.0, 1.0));
192 assert!(approx_eq(ecef.z, EARTH_RADIUS_M, 1.0));
193 }
194
195 #[test]
196 fn test_roundtrip() {
197 let original = LatLon::new(45.0, -122.0);
198 let ecef = Ecef::from_latlon(original);
199 let back = ecef.to_latlon();
200
201 assert!(approx_eq(original.lat_deg, back.lat_deg, 0.001));
202 assert!(approx_eq(original.lon_deg, back.lon_deg, 0.001));
203 }
204
205 #[test]
206 fn test_geo_aoi_contains() {
207 let aoi = GeoAoi::new(LatLon::new(50.0, -10.0), LatLon::new(40.0, 10.0));
208
209 assert!(aoi.contains(LatLon::new(45.0, 0.0)));
210 assert!(!aoi.contains(LatLon::new(60.0, 0.0)));
211 assert!(!aoi.contains(LatLon::new(45.0, 20.0)));
212 }
213
214 #[test]
215 fn test_geo_aoi_center() {
216 let aoi = GeoAoi::new(LatLon::new(50.0, -10.0), LatLon::new(40.0, 10.0));
217 let center = aoi.center();
218
219 assert!(approx_eq(center.lat_deg, 45.0, 0.001));
220 assert!(approx_eq(center.lon_deg, 0.0, 0.001));
221 }
222
223 #[test]
224 fn test_elevation_directly_above() {
225 let sat = Ecef {
227 x: EARTH_RADIUS_M + 550_000.0,
228 y: 0.0,
229 z: 0.0,
230 };
231 let station = LatLon::new(0.0, 0.0);
232 let elev = sat.elevation_from(station);
233 assert!(
234 approx_eq(elev, 90.0, 0.1),
235 "directly above: elev={}",
236 elev
237 );
238 }
239
240 #[test]
241 fn test_elevation_below_horizon() {
242 let sat = Ecef {
244 x: -(EARTH_RADIUS_M + 550_000.0),
245 y: 0.0,
246 z: 0.0,
247 };
248 let station = LatLon::new(0.0, 0.0);
249 let elev = sat.elevation_from(station);
250 assert!(elev < 0.0, "opposite side: elev={}", elev);
251 }
252}