use chrono::{DateTime, Utc};
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize)]
pub struct AstroMetadata {
pub equipment: Equipment,
pub detector: Detector,
pub filter: Filter,
pub exposure: Exposure,
pub mount: Option<Mount>,
pub environment: Option<Environment>,
pub wcs: Option<WcsData>,
pub xisf: Option<XisfMetadata>,
pub color_management: Option<ColorManagement>,
pub attachments: Vec<AttachmentInfo>,
pub raw_headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Equipment {
pub telescope_name: Option<String>,
pub focal_length: Option<f32>,
pub aperture: Option<f32>,
pub focal_ratio: Option<f32>,
pub reducer_flattener: Option<String>,
pub mount_model: Option<String>,
pub focuser_position: Option<i32>,
pub focuser_temperature: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Detector {
pub camera_name: Option<String>,
pub pixel_size: Option<f32>,
pub width: usize,
pub height: usize,
pub binning_x: usize,
pub binning_y: usize,
pub gain: Option<f32>,
pub offset: Option<i32>,
pub readout_mode: Option<String>,
pub usb_limit: Option<String>,
pub read_noise: Option<f32>,
pub full_well: Option<f32>,
pub temperature: Option<f32>,
pub temp_setpoint: Option<f32>,
pub cooler_power: Option<f32>,
pub cooler_status: Option<String>,
pub rotator_angle: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Filter {
pub name: Option<String>,
pub position: Option<usize>,
pub wavelength: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Exposure {
pub object_name: Option<String>,
pub ra: Option<f64>,
pub dec: Option<f64>,
pub date_obs: Option<DateTime<Utc>>,
pub session_date: Option<DateTime<Utc>>,
pub exposure_time: Option<f32>,
pub frame_type: Option<String>,
pub sequence_id: Option<String>,
pub frame_number: Option<usize>,
pub dither_offset_x: Option<f32>,
pub dither_offset_y: Option<f32>,
pub project_name: Option<String>,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Mount {
pub pier_side: Option<String>,
pub meridian_flip: Option<bool>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub height: Option<f64>,
pub guide_camera: Option<String>,
pub guide_rms: Option<f32>,
pub guide_scale: Option<f32>,
pub dither_enabled: Option<bool>,
pub peak_ra_error: Option<f32>,
pub peak_dec_error: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Environment {
pub ambient_temp: Option<f32>,
pub humidity: Option<f32>,
pub dew_heater_power: Option<f32>,
pub voltage: Option<f32>,
pub current: Option<f32>,
pub software_version: Option<String>,
pub plugin_info: Option<String>,
pub sqm: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct WcsData {
pub ctype1: Option<String>,
pub ctype2: Option<String>,
pub crpix1: Option<f64>,
pub crpix2: Option<f64>,
pub crval1: Option<f64>,
pub crval2: Option<f64>,
pub cd1_1: Option<f64>,
pub cd1_2: Option<f64>,
pub cd2_1: Option<f64>,
pub cd2_2: Option<f64>,
pub crota2: Option<f64>,
pub airmass: Option<f32>,
pub altitude: Option<f32>,
pub azimuth: Option<f32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct XisfMetadata {
pub version: String,
pub creator: Option<String>,
pub creation_time: Option<DateTime<Utc>>,
pub block_alignment: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ColorManagement {
pub color_space: Option<String>,
pub icc_profile: Option<Vec<u8>>,
pub display_function: Option<DisplayFunction>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct DisplayFunction {
pub function_type: Option<String>,
pub parameters: HashMap<String, f64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct AttachmentInfo {
pub id: String,
pub geometry: String,
pub sample_format: String,
pub bits_per_sample: usize,
pub compression: Option<String>,
pub compression_parameters: HashMap<String, String>,
pub checksum_type: Option<String>,
pub checksum: Option<String>,
pub resolution_x: Option<f64>,
pub resolution_y: Option<f64>,
pub resolution_unit: Option<String>,
}
impl AstroMetadata {
pub fn can_calculate_plate_scale(&self) -> bool {
self.equipment.focal_length.is_some() && self.detector.pixel_size.is_some()
}
pub fn plate_scale(&self) -> Option<f32> {
if let (Some(focal_length), Some(pixel_size)) =
(self.equipment.focal_length, self.detector.pixel_size)
{
Some((pixel_size / focal_length) * 206.265)
} else {
None
}
}
pub fn field_of_view(&self) -> Option<(f32, f32)> {
if let Some(plate_scale) = self.plate_scale() {
let width_arcmin = (self.detector.width as f32 * plate_scale) / 60.0;
let height_arcmin = (self.detector.height as f32 * plate_scale) / 60.0;
Some((width_arcmin, height_arcmin))
} else {
None
}
}
fn approximate_timezone_from_longitude(&self) -> Option<i32> {
self.mount
.as_ref()
.and_then(|mount| mount.longitude)
.map(|longitude| (longitude / 15.0).round() as i32)
}
pub fn calculate_session_date(&mut self) {
if let Some(date_obs) = self.exposure.date_obs {
let mut local_time = date_obs;
if let Some(tz_offset) = self.approximate_timezone_from_longitude() {
local_time = date_obs + chrono::Duration::hours(tz_offset as i64);
}
let naive_noon = local_time.date_naive().and_hms_opt(12, 0, 0).unwrap();
let noon = DateTime::from_naive_utc_and_offset(naive_noon, Utc);
self.exposure.session_date = if local_time < noon {
Some(noon - chrono::Duration::days(1))
} else {
Some(noon)
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_plate_scale_calculation() {
let mut metadata = AstroMetadata::default();
assert!(!metadata.can_calculate_plate_scale());
assert_eq!(metadata.plate_scale(), None);
metadata.equipment.focal_length = Some(1000.0);
assert!(!metadata.can_calculate_plate_scale());
assert_eq!(metadata.plate_scale(), None);
metadata.detector.pixel_size = Some(5.0);
assert!(metadata.can_calculate_plate_scale());
let plate_scale = metadata.plate_scale().unwrap();
assert!((plate_scale - 1.031325).abs() < 0.0001);
}
#[test]
fn test_field_of_view_calculation() {
let mut metadata = AstroMetadata::default();
metadata.equipment.focal_length = Some(1000.0);
metadata.detector.pixel_size = Some(5.0);
metadata.detector.width = 4096;
metadata.detector.height = 2160;
let fov = metadata.field_of_view().unwrap();
assert!((fov.0 - 70.48).abs() < 0.1);
assert!((fov.1 - 37.14).abs() < 0.1);
}
#[test]
fn test_timezone_from_longitude() {
let mut metadata = AstroMetadata::default();
assert_eq!(metadata.approximate_timezone_from_longitude(), None);
metadata.mount = Some(Mount {
longitude: Some(0.0),
..Default::default()
});
assert_eq!(metadata.approximate_timezone_from_longitude(), Some(0));
metadata.mount = Some(Mount {
longitude: Some(15.0),
..Default::default()
});
assert_eq!(metadata.approximate_timezone_from_longitude(), Some(1));
metadata.mount = Some(Mount {
longitude: Some(-75.0),
..Default::default()
});
assert_eq!(metadata.approximate_timezone_from_longitude(), Some(-5));
metadata.mount = Some(Mount {
longitude: Some(127.5),
..Default::default()
});
assert_eq!(metadata.approximate_timezone_from_longitude(), Some(9));
}
#[test]
fn test_calculate_session_date() {
let mut metadata = AstroMetadata::default();
metadata.calculate_session_date();
assert_eq!(metadata.exposure.session_date, None);
let obs_date = Utc.with_ymd_and_hms(2023, 5, 15, 2, 0, 0).unwrap();
metadata.exposure.date_obs = Some(obs_date);
metadata.calculate_session_date();
let expected_session = Utc.with_ymd_and_hms(2023, 5, 14, 12, 0, 0).unwrap();
assert_eq!(metadata.exposure.session_date, Some(expected_session));
let obs_date = Utc.with_ymd_and_hms(2023, 5, 15, 14, 0, 0).unwrap();
metadata.exposure.date_obs = Some(obs_date);
metadata.calculate_session_date();
let expected_session = Utc.with_ymd_and_hms(2023, 5, 15, 12, 0, 0).unwrap();
assert_eq!(metadata.exposure.session_date, Some(expected_session));
metadata.mount = Some(Mount {
longitude: Some(-75.0),
..Default::default()
});
metadata.calculate_session_date();
let expected_session = Utc.with_ymd_and_hms(2023, 5, 14, 12, 0, 0).unwrap();
assert_eq!(metadata.exposure.session_date, Some(expected_session));
}
}