use crate::access::location::PointLocation;
use crate::datasets::loaders::{
load_point_locations_from_geojson, parse_point_locations_from_geojson,
};
use crate::utils::errors::BraheError;
pub const AVAILABLE_PROVIDERS: &[&str] = &[
"atlas", "aws", "ksat", "leaf", "nasa dsn", "nasa nen", "ssc", "viasat",
];
fn get_embedded_groundstation_data(provider: &str) -> Result<&'static str, BraheError> {
match provider.to_lowercase().as_str() {
"atlas" => Ok(include_str!("../../data/groundstations/atlas.json")),
"aws" => Ok(include_str!("../../data/groundstations/aws.json")),
"ksat" => Ok(include_str!("../../data/groundstations/ksat.json")),
"leaf" => Ok(include_str!("../../data/groundstations/leaf.json")),
"nasa dsn" => Ok(include_str!("../../data/groundstations/dsn.json")),
"nasa nen" => Ok(include_str!("../../data/groundstations/nen.json")),
"ssc" => Ok(include_str!("../../data/groundstations/ssc.json")),
"viasat" => Ok(include_str!("../../data/groundstations/viasat.json")),
_ => Err(BraheError::Error(format!(
"Unknown groundstation provider '{}'. Available: {}",
provider,
AVAILABLE_PROVIDERS.join(", ")
))),
}
}
pub fn load_groundstations(provider: &str) -> Result<Vec<PointLocation>, BraheError> {
let json_str = get_embedded_groundstation_data(provider)?;
let geojson: serde_json::Value = serde_json::from_str(json_str)
.map_err(|e| BraheError::ParseError(format!("Failed to parse embedded JSON: {}", e)))?;
parse_point_locations_from_geojson(&geojson)
}
pub fn load_groundstations_from_file(filepath: &str) -> Result<Vec<PointLocation>, BraheError> {
load_point_locations_from_geojson(filepath)
}
pub fn load_all_groundstations() -> Result<Vec<PointLocation>, BraheError> {
let mut all_stations = Vec::new();
debug_assert!(
!AVAILABLE_PROVIDERS.is_empty(),
"No available groundstation providers"
);
for provider in AVAILABLE_PROVIDERS {
match load_groundstations(provider) {
Ok(mut stations) => all_stations.append(&mut stations),
Err(e) => {
eprintln!("Warning: Failed to load {} groundstations: {}", provider, e);
}
}
}
Ok(all_stations)
}
pub fn list_providers() -> Vec<String> {
AVAILABLE_PROVIDERS.iter().map(|s| s.to_string()).collect()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::access::location::AccessibleLocation;
use crate::utils::identifiable::Identifiable;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_load_ksat_groundstations() {
let stations = load_groundstations("ksat").unwrap();
assert!(
!stations.is_empty(),
"KSAT should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_atlas_groundstations() {
let stations = load_groundstations("atlas").unwrap();
assert!(
!stations.is_empty(),
"Atlas should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_aws_groundstations() {
let stations = load_groundstations("aws").unwrap();
assert!(
!stations.is_empty(),
"AWS should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_leaf_groundstations() {
let stations = load_groundstations("leaf").unwrap();
assert!(
!stations.is_empty(),
"Leaf should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_ssc_groundstations() {
let stations = load_groundstations("ssc").unwrap();
assert!(
!stations.is_empty(),
"SSC should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_viasat_groundstations() {
let stations = load_groundstations("viasat").unwrap();
assert!(
!stations.is_empty(),
"Viasat should have at least one groundstation"
);
for station in &stations {
assert!(station.lon() >= -180.0 && station.lon() <= 180.0);
assert!(station.lat() >= -90.0 && station.lat() <= 90.0);
assert!(station.get_name().is_some(), "Station should have a name");
}
}
#[test]
fn test_load_invalid_provider() {
let result = load_groundstations("nonexistent");
assert!(result.is_err(), "Should error on invalid provider");
}
#[test]
fn test_case_insensitive_provider() {
let stations1 = load_groundstations("KSAT").unwrap();
let stations2 = load_groundstations("ksat").unwrap();
let stations3 = load_groundstations("KsAt").unwrap();
assert_eq!(stations1.len(), stations2.len());
assert_eq!(stations1.len(), stations3.len());
}
#[test]
fn test_load_all_groundstations() {
let all_stations = load_all_groundstations().unwrap();
assert!(
all_stations.len() > 10,
"Should have multiple groundstations across all providers"
);
let mut total = 0;
for provider in AVAILABLE_PROVIDERS {
let stations = load_groundstations(provider).unwrap();
total += stations.len();
}
assert_eq!(
all_stations.len(),
total,
"Total should match sum of all providers"
);
}
#[test]
fn test_list_providers() {
let providers = list_providers();
assert_eq!(providers.len(), AVAILABLE_PROVIDERS.len());
for expected in AVAILABLE_PROVIDERS {
assert!(
providers.contains(&expected.to_string()),
"Should contain provider: {}",
expected
);
}
}
#[test]
fn test_groundstation_properties() {
for provider in AVAILABLE_PROVIDERS {
let stations = load_groundstations(provider).unwrap();
for station in &stations {
assert!(
station.get_name().is_some(),
"Groundstation should have a name"
);
assert!(
station.lon() >= -180.0 && station.lon() <= 180.0,
"Longitude should be valid"
);
assert!(
station.lat() >= -90.0 && station.lat() <= 90.0,
"Latitude should be valid"
);
let props = station.properties();
assert!(
props.contains_key("provider"),
"Should have provider property"
);
assert!(
props.contains_key("frequency_bands"),
"Should have frequency_bands property"
);
let freq_bands = &props["frequency_bands"];
assert!(freq_bands.is_array(), "Frequency bands should be an array");
}
}
}
#[test]
fn test_load_groundstations_from_file_valid() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-118.15, 35.05]
},
"properties": {
"name": "Test Station 1",
"provider": "Test Provider",
"frequency_bands": ["S", "X"]
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [144.82, 13.51]
},
"properties": {
"name": "Test Station 2",
"provider": "Test Provider",
"frequency_bands": ["S"]
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let stations = load_groundstations_from_file(temp_path).unwrap();
assert_eq!(stations.len(), 2);
assert_eq!(stations[0].get_name().unwrap(), "Test Station 1");
assert_eq!(stations[0].lon(), -118.15);
assert_eq!(stations[0].lat(), 35.05);
assert_eq!(stations[1].get_name().unwrap(), "Test Station 2");
assert_eq!(stations[1].lon(), 144.82);
assert_eq!(stations[1].lat(), 13.51);
}
#[test]
fn test_load_groundstations_from_file_single_station() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [10.5, 52.3]
},
"properties": {
"name": "Single Station",
"provider": "Test",
"frequency_bands": ["UHF"]
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let stations = load_groundstations_from_file(temp_path).unwrap();
assert_eq!(stations.len(), 1);
assert_eq!(stations[0].get_name().unwrap(), "Single Station");
}
#[test]
fn test_load_groundstations_from_file_with_properties() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [139.69, 35.68]
},
"properties": {
"name": "Tokyo Station",
"provider": "Custom Provider",
"frequency_bands": ["S", "X", "Ka"],
"elevation": 40,
"antenna_size": 11
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let stations = load_groundstations_from_file(temp_path).unwrap();
assert_eq!(stations.len(), 1);
let props = stations[0].properties();
assert_eq!(props["provider"].as_str().unwrap(), "Custom Provider");
assert_eq!(props["elevation"].as_i64().unwrap(), 40);
assert_eq!(props["antenna_size"].as_i64().unwrap(), 11);
}
#[test]
fn test_load_groundstations_from_file_nonexistent() {
let result = load_groundstations_from_file("/nonexistent/path/file.json");
assert!(result.is_err(), "Should error when file does not exist");
}
#[test]
fn test_load_groundstations_from_file_invalid_json() {
let invalid_json = "{ this is not valid JSON }";
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", invalid_json).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let result = load_groundstations_from_file(temp_path);
assert!(result.is_err(), "Should error on invalid JSON");
}
#[test]
fn test_load_groundstations_from_file_invalid_geojson() {
let invalid_geojson = r#"{
"type": "NotAFeatureCollection",
"data": []
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", invalid_geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let result = load_groundstations_from_file(temp_path);
assert!(result.is_err(), "Should error on invalid GeoJSON structure");
}
#[test]
fn test_load_groundstations_from_file_empty_features() {
let empty_geojson = r#"{
"type": "FeatureCollection",
"features": []
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", empty_geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let result = load_groundstations_from_file(temp_path);
assert!(
result.is_err(),
"Should error when FeatureCollection is empty"
);
}
#[test]
fn test_load_groundstations_from_file_missing_coordinates() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point"
},
"properties": {
"name": "Bad Station"
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let result = load_groundstations_from_file(temp_path);
assert!(result.is_err(), "Should error when coordinates are missing");
}
#[test]
#[should_panic(expected = "Invalid geodetic coordinates")]
fn test_load_groundstations_from_file_invalid_coordinates() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [200.0, 100.0]
},
"properties": {
"name": "Invalid Station"
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let _ = load_groundstations_from_file(temp_path);
}
#[test]
fn test_load_groundstations_from_file_mixed_valid_invalid() {
let geojson = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [0.0, 0.0]
},
"properties": {
"name": "Origin Station"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-180.0, -90.0]
},
"properties": {
"name": "South Pole Station"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [180.0, 90.0]
},
"properties": {
"name": "North Pole Station"
}
}
]
}"#;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "{}", geojson).unwrap();
temp_file.flush().unwrap();
let temp_path = temp_file.path().to_str().unwrap();
let stations = load_groundstations_from_file(temp_path).unwrap();
assert_eq!(stations.len(), 3);
assert_eq!(stations[0].lon(), 0.0);
assert_eq!(stations[0].lat(), 0.0);
assert_eq!(stations[1].lon(), -180.0);
assert_eq!(stations[1].lat(), -90.0);
assert_eq!(stations[2].lon(), 180.0);
assert_eq!(stations[2].lat(), 90.0);
}
}