use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct GeoCoordinate {
pub lat: f64,
pub lon: f64,
pub alt: f64,
}
impl GeoCoordinate {
pub fn new(lat: f64, lon: f64, alt: f64) -> Result<Self, &'static str> {
if !(-90.0..=90.0).contains(&lat) {
return Err("Latitude must be between -90 and 90 degrees");
}
if !(-180.0..=180.0).contains(&lon) {
return Err("Longitude must be between -180 and 180 degrees");
}
Ok(Self { lat, lon, alt })
}
pub fn distance_to(&self, other: &GeoCoordinate) -> f64 {
const EARTH_RADIUS: f64 = 6371000.0;
let lat1 = self.lat.to_radians();
let lat2 = other.lat.to_radians();
let delta_lat = (other.lat - self.lat).to_radians();
let delta_lon = (other.lon - self.lon).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS * c
}
pub fn distance_3d(&self, other: &GeoCoordinate) -> f64 {
let horizontal = self.distance_to(other);
let vertical = (other.alt - self.alt).abs();
(horizontal.powi(2) + vertical.powi(2)).sqrt()
}
pub fn bearing_to(&self, other: &GeoCoordinate) -> f64 {
let lat1 = self.lat.to_radians();
let lat2 = other.lat.to_radians();
let delta_lon = (other.lon - self.lon).to_radians();
let y = delta_lon.sin() * lat2.cos();
let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * delta_lon.cos();
let bearing = y.atan2(x).to_degrees();
(bearing + 360.0) % 360.0
}
}
impl From<(f64, f64, f64)> for GeoCoordinate {
fn from(tuple: (f64, f64, f64)) -> Self {
Self {
lat: tuple.0,
lon: tuple.1,
alt: tuple.2,
}
}
}
impl From<GeoCoordinate> for (f64, f64, f64) {
fn from(coord: GeoCoordinate) -> Self {
(coord.lat, coord.lon, coord.alt)
}
}
impl fmt::Display for GeoCoordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:.6}°{}, {:.6}°{}, {:.1}m",
self.lat.abs(),
if self.lat >= 0.0 { "N" } else { "S" },
self.lon.abs(),
if self.lon >= 0.0 { "E" } else { "W" },
self.alt
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationalBox {
pub id: String,
pub southwest: GeoCoordinate,
pub northeast: GeoCoordinate,
pub min_altitude: f64,
pub max_altitude: f64,
pub name: Option<String>,
}
impl OperationalBox {
pub fn new(
id: String,
southwest: GeoCoordinate,
northeast: GeoCoordinate,
min_altitude: f64,
max_altitude: f64,
) -> Result<Self, &'static str> {
if southwest.lat >= northeast.lat {
return Err("Southwest latitude must be less than northeast latitude");
}
if southwest.lon >= northeast.lon {
return Err("Southwest longitude must be less than northeast longitude");
}
if min_altitude >= max_altitude {
return Err("Minimum altitude must be less than maximum altitude");
}
Ok(Self {
id,
southwest,
northeast,
min_altitude,
max_altitude,
name: None,
})
}
pub fn from_center(
id: String,
center: GeoCoordinate,
width_meters: f64,
height_meters: f64,
altitude_range: (f64, f64),
) -> Result<Self, &'static str> {
let meters_per_degree_lat = 111320.0;
let meters_per_degree_lon = 111320.0 * center.lat.to_radians().cos();
let half_width_deg = (width_meters / 2.0) / meters_per_degree_lon;
let half_height_deg = (height_meters / 2.0) / meters_per_degree_lat;
let southwest = GeoCoordinate::new(
center.lat - half_height_deg,
center.lon - half_width_deg,
altitude_range.0,
)?;
let northeast = GeoCoordinate::new(
center.lat + half_height_deg,
center.lon + half_width_deg,
altitude_range.1,
)?;
Self::new(id, southwest, northeast, altitude_range.0, altitude_range.1)
}
pub fn contains(&self, coord: &GeoCoordinate) -> bool {
coord.lat >= self.southwest.lat
&& coord.lat <= self.northeast.lat
&& coord.lon >= self.southwest.lon
&& coord.lon <= self.northeast.lon
&& coord.alt >= self.min_altitude
&& coord.alt <= self.max_altitude
}
pub fn center(&self) -> GeoCoordinate {
GeoCoordinate {
lat: (self.southwest.lat + self.northeast.lat) / 2.0,
lon: (self.southwest.lon + self.northeast.lon) / 2.0,
alt: (self.min_altitude + self.max_altitude) / 2.0,
}
}
pub fn width(&self) -> f64 {
let sw_ne = GeoCoordinate::new(self.southwest.lat, self.northeast.lon, 0.0).unwrap();
self.southwest.distance_to(&sw_ne)
}
pub fn height(&self) -> f64 {
let sw_ne = GeoCoordinate::new(self.northeast.lat, self.southwest.lon, 0.0).unwrap();
self.southwest.distance_to(&sw_ne)
}
pub fn area(&self) -> f64 {
self.width() * self.height()
}
pub fn volume(&self) -> f64 {
self.area() * (self.max_altitude - self.min_altitude)
}
pub fn subdivide(&self, rows: usize, cols: usize) -> Vec<OperationalBox> {
let lat_step = (self.northeast.lat - self.southwest.lat) / rows as f64;
let lon_step = (self.northeast.lon - self.southwest.lon) / cols as f64;
let mut boxes = Vec::new();
for row in 0..rows {
for col in 0..cols {
let sw_lat = self.southwest.lat + (row as f64 * lat_step);
let sw_lon = self.southwest.lon + (col as f64 * lon_step);
let ne_lat = sw_lat + lat_step;
let ne_lon = sw_lon + lon_step;
let sw = GeoCoordinate::new(sw_lat, sw_lon, self.min_altitude).unwrap();
let ne = GeoCoordinate::new(ne_lat, ne_lon, self.max_altitude).unwrap();
let sub_box = OperationalBox::new(
format!("{}_{}_{}", self.id, row, col),
sw,
ne,
self.min_altitude,
self.max_altitude,
)
.unwrap();
boxes.push(sub_box);
}
}
boxes
}
}
impl fmt::Display for OperationalBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"OperationalBox[{}]: {} to {}, alt {:.0}-{:.0}m ({:.1}km²)",
self.id,
self.southwest,
self.northeast,
self.min_altitude,
self.max_altitude,
self.area() / 1_000_000.0
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_geocoordinate_creation() {
let coord = GeoCoordinate::new(37.7749, -122.4194, 100.0).unwrap();
assert_eq!(coord.lat, 37.7749);
assert_eq!(coord.lon, -122.4194);
assert_eq!(coord.alt, 100.0);
assert!(GeoCoordinate::new(91.0, 0.0, 0.0).is_err());
assert!(GeoCoordinate::new(-91.0, 0.0, 0.0).is_err());
assert!(GeoCoordinate::new(0.0, 181.0, 0.0).is_err());
assert!(GeoCoordinate::new(0.0, -181.0, 0.0).is_err());
}
#[test]
fn test_distance_calculation() {
let sf = GeoCoordinate::new(37.7749, -122.4194, 0.0).unwrap();
let la = GeoCoordinate::new(34.0522, -118.2437, 0.0).unwrap();
let distance = sf.distance_to(&la);
assert!((distance - 559_000.0).abs() < 5000.0); }
#[test]
fn test_bearing_calculation() {
let coord1 = GeoCoordinate::new(0.0, 0.0, 0.0).unwrap();
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);
let bearing_east = coord1.bearing_to(&coord3);
assert!((bearing_north - 0.0).abs() < 1.0); assert!((bearing_east - 90.0).abs() < 1.0); }
#[test]
fn test_operational_box_creation() {
let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
let op_box = OperationalBox::new("test_box".to_string(), sw, ne, 0.0, 1000.0).unwrap();
assert_eq!(op_box.id, "test_box");
assert_eq!(op_box.southwest.lat, 37.0);
assert_eq!(op_box.northeast.lat, 38.0);
}
#[test]
fn test_operational_box_contains() {
let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
let inside = GeoCoordinate::new(37.5, -121.5, 500.0).unwrap();
let outside = GeoCoordinate::new(36.5, -121.5, 500.0).unwrap();
assert!(op_box.contains(&inside));
assert!(!op_box.contains(&outside));
}
#[test]
fn test_operational_box_center() {
let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
let center = op_box.center();
assert_eq!(center.lat, 37.5);
assert_eq!(center.lon, -121.5);
assert_eq!(center.alt, 500.0);
}
#[test]
fn test_operational_box_from_center() {
let center = GeoCoordinate::new(37.5, -121.5, 500.0).unwrap();
let op_box = OperationalBox::from_center(
"test".to_string(),
center,
10000.0, 20000.0, (0.0, 1000.0),
)
.unwrap();
let box_center = op_box.center();
assert!((box_center.lat - center.lat).abs() < 0.01);
assert!((box_center.lon - center.lon).abs() < 0.01);
}
#[test]
fn test_operational_box_subdivide() {
let sw = GeoCoordinate::new(37.0, -122.0, 0.0).unwrap();
let ne = GeoCoordinate::new(38.0, -121.0, 0.0).unwrap();
let op_box = OperationalBox::new("test".to_string(), sw, ne, 0.0, 1000.0).unwrap();
let sub_boxes = op_box.subdivide(2, 2);
assert_eq!(sub_boxes.len(), 4);
for sub_box in &sub_boxes {
assert!(op_box.contains(&sub_box.center()));
}
}
}