1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct GeoCoordinate {
12 pub lat: f64,
14 pub lon: f64,
16 pub alt: f64,
18}
19
20impl GeoCoordinate {
21 pub fn new(lat: f64, lon: f64, alt: f64) -> Result<Self, &'static str> {
23 if !(-90.0..=90.0).contains(&lat) {
24 return Err("Latitude must be between -90 and 90 degrees");
25 }
26 if !(-180.0..=180.0).contains(&lon) {
27 return Err("Longitude must be between -180 and 180 degrees");
28 }
29 Ok(Self { lat, lon, alt })
30 }
31
32 pub fn distance_to(&self, other: &GeoCoordinate) -> f64 {
34 const EARTH_RADIUS: f64 = 6371000.0; let lat1 = self.lat.to_radians();
37 let lat2 = other.lat.to_radians();
38 let delta_lat = (other.lat - self.lat).to_radians();
39 let delta_lon = (other.lon - self.lon).to_radians();
40
41 let a = (delta_lat / 2.0).sin().powi(2)
42 + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
43 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
44
45 EARTH_RADIUS * c
46 }
47
48 pub fn distance_3d(&self, other: &GeoCoordinate) -> f64 {
50 let horizontal = self.distance_to(other);
51 let vertical = (other.alt - self.alt).abs();
52 (horizontal.powi(2) + vertical.powi(2)).sqrt()
53 }
54
55 pub fn bearing_to(&self, other: &GeoCoordinate) -> f64 {
57 let lat1 = self.lat.to_radians();
58 let lat2 = other.lat.to_radians();
59 let delta_lon = (other.lon - self.lon).to_radians();
60
61 let y = delta_lon.sin() * lat2.cos();
62 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * delta_lon.cos();
63 let bearing = y.atan2(x).to_degrees();
64
65 (bearing + 360.0) % 360.0
66 }
67}
68
69impl From<(f64, f64, f64)> for GeoCoordinate {
70 fn from(tuple: (f64, f64, f64)) -> Self {
71 Self {
72 lat: tuple.0,
73 lon: tuple.1,
74 alt: tuple.2,
75 }
76 }
77}
78
79impl From<GeoCoordinate> for (f64, f64, f64) {
80 fn from(coord: GeoCoordinate) -> Self {
81 (coord.lat, coord.lon, coord.alt)
82 }
83}
84
85impl fmt::Display for GeoCoordinate {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(
88 f,
89 "{:.6}°{}, {:.6}°{}, {:.1}m",
90 self.lat.abs(),
91 if self.lat >= 0.0 { "N" } else { "S" },
92 self.lon.abs(),
93 if self.lon >= 0.0 { "E" } else { "W" },
94 self.alt
95 )
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct OperationalBox {
105 pub id: String,
107
108 pub southwest: GeoCoordinate,
110
111 pub northeast: GeoCoordinate,
113
114 pub min_altitude: f64,
116
117 pub max_altitude: f64,
119
120 pub name: Option<String>,
122}
123
124impl OperationalBox {
125 pub fn new(
127 id: String,
128 southwest: GeoCoordinate,
129 northeast: GeoCoordinate,
130 min_altitude: f64,
131 max_altitude: f64,
132 ) -> Result<Self, &'static str> {
133 if southwest.lat >= northeast.lat {
135 return Err("Southwest latitude must be less than northeast latitude");
136 }
137 if southwest.lon >= northeast.lon {
138 return Err("Southwest longitude must be less than northeast longitude");
139 }
140 if min_altitude >= max_altitude {
141 return Err("Minimum altitude must be less than maximum altitude");
142 }
143
144 Ok(Self {
145 id,
146 southwest,
147 northeast,
148 min_altitude,
149 max_altitude,
150 name: None,
151 })
152 }
153
154 pub fn from_center(
156 id: String,
157 center: GeoCoordinate,
158 width_meters: f64,
159 height_meters: f64,
160 altitude_range: (f64, f64),
161 ) -> Result<Self, &'static str> {
162 let meters_per_degree_lat = 111320.0;
164 let meters_per_degree_lon = 111320.0 * center.lat.to_radians().cos();
165
166 let half_width_deg = (width_meters / 2.0) / meters_per_degree_lon;
167 let half_height_deg = (height_meters / 2.0) / meters_per_degree_lat;
168
169 let southwest = GeoCoordinate::new(
170 center.lat - half_height_deg,
171 center.lon - half_width_deg,
172 altitude_range.0,
173 )?;
174
175 let northeast = GeoCoordinate::new(
176 center.lat + half_height_deg,
177 center.lon + half_width_deg,
178 altitude_range.1,
179 )?;
180
181 Self::new(id, southwest, northeast, altitude_range.0, altitude_range.1)
182 }
183
184 pub fn contains(&self, coord: &GeoCoordinate) -> bool {
186 coord.lat >= self.southwest.lat
187 && coord.lat <= self.northeast.lat
188 && coord.lon >= self.southwest.lon
189 && coord.lon <= self.northeast.lon
190 && coord.alt >= self.min_altitude
191 && coord.alt <= self.max_altitude
192 }
193
194 pub fn center(&self) -> GeoCoordinate {
196 GeoCoordinate {
197 lat: (self.southwest.lat + self.northeast.lat) / 2.0,
198 lon: (self.southwest.lon + self.northeast.lon) / 2.0,
199 alt: (self.min_altitude + self.max_altitude) / 2.0,
200 }
201 }
202
203 pub fn width(&self) -> f64 {
205 let sw_ne = GeoCoordinate::new(self.southwest.lat, self.northeast.lon, 0.0).unwrap();
206 self.southwest.distance_to(&sw_ne)
207 }
208
209 pub fn height(&self) -> f64 {
211 let sw_ne = GeoCoordinate::new(self.northeast.lat, self.southwest.lon, 0.0).unwrap();
212 self.southwest.distance_to(&sw_ne)
213 }
214
215 pub fn area(&self) -> f64 {
217 self.width() * self.height()
218 }
219
220 pub fn volume(&self) -> f64 {
222 self.area() * (self.max_altitude - self.min_altitude)
223 }
224
225 pub fn subdivide(&self, rows: usize, cols: usize) -> Vec<OperationalBox> {
227 let lat_step = (self.northeast.lat - self.southwest.lat) / rows as f64;
228 let lon_step = (self.northeast.lon - self.southwest.lon) / cols as f64;
229
230 let mut boxes = Vec::new();
231
232 for row in 0..rows {
233 for col in 0..cols {
234 let sw_lat = self.southwest.lat + (row as f64 * lat_step);
235 let sw_lon = self.southwest.lon + (col as f64 * lon_step);
236 let ne_lat = sw_lat + lat_step;
237 let ne_lon = sw_lon + lon_step;
238
239 let sw = GeoCoordinate::new(sw_lat, sw_lon, self.min_altitude).unwrap();
240 let ne = GeoCoordinate::new(ne_lat, ne_lon, self.max_altitude).unwrap();
241
242 let sub_box = OperationalBox::new(
243 format!("{}_{}_{}", self.id, row, col),
244 sw,
245 ne,
246 self.min_altitude,
247 self.max_altitude,
248 )
249 .unwrap();
250
251 boxes.push(sub_box);
252 }
253 }
254
255 boxes
256 }
257}
258
259impl fmt::Display for OperationalBox {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 write!(
262 f,
263 "OperationalBox[{}]: {} to {}, alt {:.0}-{:.0}m ({:.1}km²)",
264 self.id,
265 self.southwest,
266 self.northeast,
267 self.min_altitude,
268 self.max_altitude,
269 self.area() / 1_000_000.0
270 )
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_geocoordinate_creation() {
280 let coord = GeoCoordinate::new(37.7749, -122.4194, 100.0).unwrap();
281 assert_eq!(coord.lat, 37.7749);
282 assert_eq!(coord.lon, -122.4194);
283 assert_eq!(coord.alt, 100.0);
284
285 assert!(GeoCoordinate::new(91.0, 0.0, 0.0).is_err());
287 assert!(GeoCoordinate::new(-91.0, 0.0, 0.0).is_err());
288
289 assert!(GeoCoordinate::new(0.0, 181.0, 0.0).is_err());
291 assert!(GeoCoordinate::new(0.0, -181.0, 0.0).is_err());
292 }
293
294 #[test]
295 fn test_distance_calculation() {
296 let sf = GeoCoordinate::new(37.7749, -122.4194, 0.0).unwrap();
298 let la = GeoCoordinate::new(34.0522, -118.2437, 0.0).unwrap();
299
300 let distance = sf.distance_to(&la);
301 assert!((distance - 559_000.0).abs() < 5000.0); }
303
304 #[test]
305 fn test_bearing_calculation() {
306 let coord1 = GeoCoordinate::new(0.0, 0.0, 0.0).unwrap();
307 let coord2 = GeoCoordinate::new(1.0, 0.0, 0.0).unwrap(); let coord3 = GeoCoordinate::new(0.0, 1.0, 0.0).unwrap(); let bearing_north = coord1.bearing_to(&coord2);
311 let bearing_east = coord1.bearing_to(&coord3);
312
313 assert!((bearing_north - 0.0).abs() < 1.0); assert!((bearing_east - 90.0).abs() < 1.0); }
316
317 #[test]
318 fn test_operational_box_creation() {
319 let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
320 let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
321
322 let op_box = OperationalBox::new("test_box".to_string(), sw, ne, 0.0, 1000.0).unwrap();
323
324 assert_eq!(op_box.id, "test_box");
325 assert_eq!(op_box.southwest.lat, 37.0);
326 assert_eq!(op_box.northeast.lat, 38.0);
327 }
328
329 #[test]
330 fn test_operational_box_contains() {
331 let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
332 let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
333 let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
334
335 let inside = GeoCoordinate::new(37.5, -121.5, 500.0).unwrap();
336 let outside = GeoCoordinate::new(36.5, -121.5, 500.0).unwrap();
337
338 assert!(op_box.contains(&inside));
339 assert!(!op_box.contains(&outside));
340 }
341
342 #[test]
343 fn test_operational_box_center() {
344 let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
345 let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
346 let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
347
348 let center = op_box.center();
349 assert_eq!(center.lat, 37.5);
350 assert_eq!(center.lon, -121.5);
351 assert_eq!(center.alt, 500.0);
352 }
353
354 #[test]
355 fn test_operational_box_from_center() {
356 let center = GeoCoordinate::new(37.5, -121.5, 500.0).unwrap();
357 let op_box = OperationalBox::from_center(
358 "test".to_string(),
359 center,
360 10000.0, 20000.0, (0.0, 1000.0),
363 )
364 .unwrap();
365
366 let box_center = op_box.center();
367 assert!((box_center.lat - center.lat).abs() < 0.01);
368 assert!((box_center.lon - center.lon).abs() < 0.01);
369 }
370
371 #[test]
372 fn test_operational_box_subdivide() {
373 let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
374 let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
375 let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
376
377 let sub_boxes = op_box.subdivide(2, 2);
378 assert_eq!(sub_boxes.len(), 4);
379
380 for sub_box in &sub_boxes {
382 assert!(op_box.contains(&sub_box.center()));
383 }
384 }
385}