use crate::coordinates::Equatorial;
pub mod features;
mod gaia;
pub mod hipparcos;
pub mod minimal_catalog;
pub mod synthetic;
pub use features::{FeatureCatalog, FeatureType, SkyFeature};
pub use gaia::{GaiaCatalog, GaiaEntry};
pub use hipparcos::{HipparcosCatalog, HipparcosEntry};
pub use minimal_catalog::{MinimalCatalog, MinimalStar};
pub use synthetic::{
create_fov_catalog, create_synthetic_catalog, MagnitudeDistribution, SpatialDistribution,
SyntheticCatalogConfig,
};
use rand::distr::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::path::PathBuf;
pub trait StarPosition {
fn ra(&self) -> f64;
fn dec(&self) -> f64;
}
#[derive(Debug, Clone, Copy)]
pub struct StarData {
pub id: u64,
pub position: Equatorial,
pub magnitude: f64,
pub b_v: Option<f64>,
}
impl StarData {
pub fn new(id: u64, ra_deg: f64, dec_deg: f64, magnitude: f64, b_v: Option<f64>) -> Self {
Self {
id,
position: Equatorial::from_degrees(ra_deg, dec_deg),
magnitude,
b_v,
}
}
pub fn with_position(id: u64, position: Equatorial, magnitude: f64, b_v: Option<f64>) -> Self {
Self {
id,
position,
magnitude,
b_v,
}
}
pub fn ra_deg(&self) -> f64 {
self.position.ra_degrees()
}
pub fn dec_deg(&self) -> f64 {
self.position.dec_degrees()
}
}
impl StarPosition for StarData {
fn ra(&self) -> f64 {
self.ra_deg()
}
fn dec(&self) -> f64 {
self.dec_deg()
}
}
pub trait StarCatalog {
type Star;
fn get_star(&self, id: usize) -> Option<&Self::Star>;
fn stars(&self) -> impl Iterator<Item = &Self::Star>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn filter<F>(&self, predicate: F) -> Vec<&Self::Star>
where
F: Fn(&Self::Star) -> bool;
fn star_data(&self) -> impl Iterator<Item = StarData> + '_;
fn filter_star_data<F>(&self, predicate: F) -> Vec<StarData>
where
F: Fn(&StarData) -> bool;
fn brighter_than(&self, magnitude: f64) -> Vec<StarData> {
self.filter_star_data(|star| star.magnitude <= magnitude)
}
fn stars_in_field(&self, ra_deg: f64, dec_deg: f64, fov_deg: f64) -> Vec<StarData> {
let center = Equatorial::from_degrees(ra_deg, dec_deg);
let radius_rad = (fov_deg / 2.0).to_radians();
let cos_radius = radius_rad.cos();
self.filter_star_data(|star| {
let cos_dist = star.position.dec.sin() * center.dec.sin()
+ star.position.dec.cos() * center.dec.cos() * (star.position.ra - center.ra).cos();
cos_dist > cos_radius
})
}
}
#[derive(Debug, Clone)]
pub enum CatalogSource {
Hipparcos,
Minimal(PathBuf),
Random { seed: u64, count: usize },
}
pub fn get_stars_in_window(
source: CatalogSource,
position: Equatorial,
fov_deg: f64,
) -> crate::Result<Vec<StarData>> {
let ra_deg = position.ra_degrees();
let dec_deg = position.dec_degrees();
match source {
CatalogSource::Random { seed, count } => {
println!("Using synthetic stars ({})", count);
println!("Seed: {}", seed);
Ok(generate_synthetic_stars(
count, ra_deg, dec_deg, fov_deg, seed,
))
}
CatalogSource::Minimal(path) => {
println!("Loading minimal catalog from: {}", path.display());
let catalog = MinimalCatalog::load(&path)?;
println!("Loaded catalog: {}", catalog.description());
println!("Total stars in catalog: {}", catalog.len());
let stars = catalog.stars_in_field(ra_deg, dec_deg, fov_deg);
println!("Found {} stars in field of view", stars.len());
Ok(stars)
}
CatalogSource::Hipparcos => {
let path = PathBuf::from("hip_main.dat");
if !path.exists() {
return Err(crate::StarfieldError::DataError(format!(
"Hipparcos catalog not found at: {}",
path.display()
)));
}
println!("Loading Hipparcos catalog from: {}", path.display());
let catalog = HipparcosCatalog::from_dat_file(&path, 8.0)?;
println!("Total stars in catalog: {}", catalog.len());
let stars = catalog.stars_in_field(ra_deg, dec_deg, fov_deg);
println!("Found {} stars in field of view", stars.len());
Ok(stars)
}
}
}
fn generate_synthetic_stars(
count: usize,
center_ra: f64,
center_dec: f64,
fov_deg: f64,
seed: u64,
) -> Vec<StarData> {
let mut rng = StdRng::seed_from_u64(seed);
let mut stars = Vec::with_capacity(count);
let half_fov = fov_deg / 2.0;
let ra_dist = Uniform::new(center_ra - half_fov, center_ra + half_fov).unwrap();
let dec_dist = Uniform::new(center_dec - half_fov, center_dec + half_fov).unwrap();
let min_mag = 3.0; let max_mag = 8.0;
let uniform = Uniform::new(0.0, 1.0).unwrap();
for id in 1..=count {
let ra = ra_dist.sample(&mut rng);
let dec = dec_dist.sample(&mut rng);
let u = uniform.sample(&mut rng);
let log_base: f64 = 2.5; let exp_range = log_base.powf(max_mag - min_mag) - 1.0;
let t: f64 = u * exp_range + 1.0;
let magnitude = min_mag + t.log(log_base).clamp(0.0, max_mag - min_mag);
let b_v = if uniform.sample(&mut rng) > 0.3 {
Some(uniform.sample(&mut rng) * 2.0 - 0.3) } else {
None
};
stars.push(StarData::new(id as u64, ra, dec, magnitude, b_v));
}
stars
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalogs::minimal_catalog::{MinimalCatalog, MinimalStar};
#[test]
fn test_star_data() {
let stars = vec![
MinimalStar::new(1, 100.0, 10.0, -1.5), MinimalStar::new(2, 50.0, -20.0, 0.5), MinimalStar::new(3, 150.0, 30.0, 1.2), MinimalStar::new(4, 200.0, -45.0, 3.7), MinimalStar::new(5, 250.0, 60.0, 5.9), ];
let catalog = MinimalCatalog::from_stars(stars, "Test catalog");
let star_data: Vec<StarData> = catalog.star_data().collect();
assert_eq!(star_data.len(), 5);
let bright_stars = catalog.brighter_than(1.0);
assert_eq!(bright_stars.len(), 2);
let brightest = star_data
.iter()
.min_by(|a, b| a.magnitude.partial_cmp(&b.magnitude).unwrap())
.unwrap();
assert_eq!(brightest.magnitude, -1.5);
}
}