use pyo3::prelude::*;
use siderust::qtty::*;
use siderust::time::{Interval, ModifiedJulianDate};
use siderust::{AltitudeProvider, SearchOpts};
use crate::bodies::{dispatch_body, PyBody};
use crate::coordinates::PyDirection;
use crate::errors::invalid_period_error;
use crate::events::{PyCrossingEvent, PyCulminationEvent};
use crate::observer::PyObserver;
use crate::star::PyStar;
enum Subject<'a> {
Body(PyBody),
Star(&'a PyStar),
Dir(&'a PyDirection),
}
fn extract_subject<'a>(target: &'a Bound<'_, PyAny>) -> PyResult<Subject<'a>> {
if let Ok(body) = target.extract::<PyBody>() {
return Ok(Subject::Body(body));
}
if let Ok(star) = target.extract::<PyRef<'_, PyStar>>() {
return Ok(Subject::Star(unsafe { &*(star.as_ptr() as *const PyStar) }));
}
if let Ok(dir) = target.extract::<PyRef<'_, PyDirection>>() {
return Ok(Subject::Dir(unsafe {
&*(dir.as_ptr() as *const PyDirection)
}));
}
Err(pyo3::exceptions::PyTypeError::new_err(
"target must be a Body, Star, or Direction",
))
}
fn make_window(start_mjd: f64, end_mjd: f64) -> PyResult<Interval<ModifiedJulianDate>> {
if start_mjd >= end_mjd {
return Err(invalid_period_error());
}
Ok(Interval::new(
ModifiedJulianDate::new(start_mjd),
ModifiedJulianDate::new(end_mjd),
))
}
#[pyfunction]
pub fn altitude_at(target: &Bound<'_, PyAny>, observer: &PyObserver, mjd: f64) -> PyResult<f64> {
let t = ModifiedJulianDate::new(mjd);
match extract_subject(target)? {
Subject::Body(body) => Ok(body.altitude_at_inner(&observer.inner, t)),
Subject::Star(star) => Ok(star
.inner
.altitude_at(&observer.inner, t)
.to::<Degree>()
.value()),
Subject::Dir(dir) => Ok(dir
.inner
.altitude_at(&observer.inner, t)
.to::<Degree>()
.value()),
}
}
#[pyfunction]
pub fn azimuth_at(target: &Bound<'_, PyAny>, observer: &PyObserver, mjd: f64) -> PyResult<f64> {
use siderust::AzimuthProvider;
let t = ModifiedJulianDate::new(mjd);
match extract_subject(target)? {
Subject::Body(body) => Ok(body.azimuth_at_inner(&observer.inner, t)),
Subject::Star(star) => Ok(star
.inner
.azimuth_at(&observer.inner, t)
.to::<Degree>()
.value()),
Subject::Dir(dir) => Ok(dir
.inner
.azimuth_at(&observer.inner, t)
.to::<Degree>()
.value()),
}
}
#[pyfunction]
pub fn above_threshold(
target: &Bound<'_, PyAny>,
observer: &PyObserver,
start_mjd: f64,
end_mjd: f64,
threshold_deg: f64,
) -> PyResult<Vec<(f64, f64)>> {
let window = make_window(start_mjd, end_mjd)?;
let threshold = Degrees::new(threshold_deg);
let opts = SearchOpts::default();
let periods = match extract_subject(target)? {
Subject::Body(body) => dispatch_body!(body, |p| {
siderust::above_threshold(&p, &observer.inner, window, threshold, opts)
}),
Subject::Star(star) => {
siderust::above_threshold(&star.inner, &observer.inner, window, threshold, opts)
}
Subject::Dir(dir) => {
siderust::above_threshold(&dir.inner, &observer.inner, window, threshold, opts)
}
};
Ok(periods
.into_iter()
.map(|p| (p.start.value(), p.end.value()))
.collect())
}
#[pyfunction]
pub fn below_threshold(
target: &Bound<'_, PyAny>,
observer: &PyObserver,
start_mjd: f64,
end_mjd: f64,
threshold_deg: f64,
) -> PyResult<Vec<(f64, f64)>> {
let window = make_window(start_mjd, end_mjd)?;
let threshold = Degrees::new(threshold_deg);
let opts = SearchOpts::default();
let periods = match extract_subject(target)? {
Subject::Body(body) => dispatch_body!(body, |p| {
siderust::below_threshold(&p, &observer.inner, window, threshold, opts)
}),
Subject::Star(star) => {
siderust::below_threshold(&star.inner, &observer.inner, window, threshold, opts)
}
Subject::Dir(dir) => {
siderust::below_threshold(&dir.inner, &observer.inner, window, threshold, opts)
}
};
Ok(periods
.into_iter()
.map(|p| (p.start.value(), p.end.value()))
.collect())
}
#[pyfunction]
pub fn crossings(
target: &Bound<'_, PyAny>,
observer: &PyObserver,
start_mjd: f64,
end_mjd: f64,
threshold_deg: f64,
) -> PyResult<Vec<PyCrossingEvent>> {
let window = make_window(start_mjd, end_mjd)?;
let threshold = Degrees::new(threshold_deg);
let opts = SearchOpts::default();
let events = match extract_subject(target)? {
Subject::Body(body) => dispatch_body!(body, |p| {
siderust::crossings(&p, &observer.inner, window, threshold, opts)
}),
Subject::Star(star) => {
siderust::crossings(&star.inner, &observer.inner, window, threshold, opts)
}
Subject::Dir(dir) => {
siderust::crossings(&dir.inner, &observer.inner, window, threshold, opts)
}
};
Ok(events.into_iter().map(PyCrossingEvent::from).collect())
}
#[pyfunction]
pub fn culminations(
target: &Bound<'_, PyAny>,
observer: &PyObserver,
start_mjd: f64,
end_mjd: f64,
) -> PyResult<Vec<PyCulminationEvent>> {
let window = make_window(start_mjd, end_mjd)?;
let opts = SearchOpts::default();
let events = match extract_subject(target)? {
Subject::Body(body) => dispatch_body!(body, |p| {
siderust::culminations(&p, &observer.inner, window, opts)
}),
Subject::Star(star) => siderust::culminations(&star.inner, &observer.inner, window, opts),
Subject::Dir(dir) => siderust::culminations(&dir.inner, &observer.inner, window, opts),
};
Ok(events.into_iter().map(PyCulminationEvent::from).collect())
}
pub const TWILIGHT_HORIZON: f64 = 0.0;
pub const TWILIGHT_CIVIL: f64 = -6.0;
pub const TWILIGHT_NAUTICAL: f64 = -12.0;
pub const TWILIGHT_ASTRONOMICAL: f64 = -18.0;
#[pyfunction]
pub fn intersect_periods(
periods_a: Vec<(f64, f64)>,
periods_b: Vec<(f64, f64)>,
) -> Vec<(f64, f64)> {
let mut result = Vec::new();
for &(a_start, a_end) in &periods_a {
for &(b_start, b_end) in &periods_b {
let start = a_start.max(b_start);
let end = a_end.min(b_end);
if start < end {
result.push((start, end));
}
}
}
result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
result
}