use pyo3::prelude::*;
use siderust::ephemeris::Vsop87Ephemeris;
use siderust::event::lunar::phase as lunar;
use siderust::qtty::*;
use siderust::time::{Interval, JulianDate, ModifiedJulianDate};
use crate::errors::invalid_period_error;
use crate::observer::PyObserver;
#[pyclass(
name = "MoonPhaseLabel",
module = "siderust",
eq,
eq_int,
hash,
frozen,
from_py_object
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PyMoonPhaseLabel {
NewMoon = 0,
WaxingCrescent = 1,
FirstQuarter = 2,
WaxingGibbous = 3,
FullMoon = 4,
WaningGibbous = 5,
LastQuarter = 6,
WaningCrescent = 7,
}
impl From<lunar::MoonPhaseLabel> for PyMoonPhaseLabel {
fn from(l: lunar::MoonPhaseLabel) -> Self {
match l {
lunar::MoonPhaseLabel::NewMoon => Self::NewMoon,
lunar::MoonPhaseLabel::WaxingCrescent => Self::WaxingCrescent,
lunar::MoonPhaseLabel::FirstQuarter => Self::FirstQuarter,
lunar::MoonPhaseLabel::WaxingGibbous => Self::WaxingGibbous,
lunar::MoonPhaseLabel::FullMoon => Self::FullMoon,
lunar::MoonPhaseLabel::WaningGibbous => Self::WaningGibbous,
lunar::MoonPhaseLabel::LastQuarter => Self::LastQuarter,
lunar::MoonPhaseLabel::WaningCrescent => Self::WaningCrescent,
}
}
}
#[pymethods]
impl PyMoonPhaseLabel {
fn __repr__(&self) -> String {
format!("MoonPhaseLabel.{:?}", self)
}
fn __str__(&self) -> &'static str {
match self {
Self::NewMoon => "New Moon",
Self::WaxingCrescent => "Waxing Crescent",
Self::FirstQuarter => "First Quarter",
Self::WaxingGibbous => "Waxing Gibbous",
Self::FullMoon => "Full Moon",
Self::WaningGibbous => "Waning Gibbous",
Self::LastQuarter => "Last Quarter",
Self::WaningCrescent => "Waning Crescent",
}
}
}
#[pyclass(name = "MoonPhaseGeometry", module = "siderust", from_py_object)]
#[derive(Clone)]
pub struct PyMoonPhaseGeometry {
pub phase_angle_deg: f64,
pub illuminated_fraction: f64,
pub elongation_deg: f64,
pub waxing: bool,
pub label: PyMoonPhaseLabel,
}
impl From<lunar::MoonPhaseGeometry> for PyMoonPhaseGeometry {
fn from(g: lunar::MoonPhaseGeometry) -> Self {
Self {
phase_angle_deg: g.phase_angle.to::<Degree>().value(),
illuminated_fraction: g.illuminated_fraction.value(),
elongation_deg: g.elongation.to::<Degree>().value(),
waxing: g.waxing,
label: g.label().into(),
}
}
}
#[pymethods]
impl PyMoonPhaseGeometry {
#[getter]
fn phase_angle_deg(&self) -> f64 {
self.phase_angle_deg
}
#[getter]
fn illuminated_fraction(&self) -> f64 {
self.illuminated_fraction
}
#[getter]
fn elongation_deg(&self) -> f64 {
self.elongation_deg
}
#[getter]
fn waxing(&self) -> bool {
self.waxing
}
#[getter]
fn label(&self) -> PyMoonPhaseLabel {
self.label
}
fn __repr__(&self) -> String {
format!(
"MoonPhaseGeometry(phase={:.1}°, illum={:.1}%, label={:?})",
self.phase_angle_deg,
self.illuminated_fraction * 100.0,
self.label
)
}
fn __str__(&self) -> String {
format!(
"{} ({:.1}% illuminated)",
self.label.__str__(),
self.illuminated_fraction * 100.0
)
}
}
#[pyclass(
name = "PhaseKind",
module = "siderust",
eq,
eq_int,
hash,
frozen,
from_py_object
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PyPhaseKind {
NewMoon = 0,
FirstQuarter = 1,
FullMoon = 2,
LastQuarter = 3,
}
impl From<lunar::PhaseKind> for PyPhaseKind {
fn from(k: lunar::PhaseKind) -> Self {
match k {
lunar::PhaseKind::NewMoon => Self::NewMoon,
lunar::PhaseKind::FirstQuarter => Self::FirstQuarter,
lunar::PhaseKind::FullMoon => Self::FullMoon,
lunar::PhaseKind::LastQuarter => Self::LastQuarter,
}
}
}
#[pymethods]
impl PyPhaseKind {
fn __repr__(&self) -> String {
format!("PhaseKind.{:?}", self)
}
fn __str__(&self) -> &'static str {
match self {
Self::NewMoon => "New Moon",
Self::FirstQuarter => "First Quarter",
Self::FullMoon => "Full Moon",
Self::LastQuarter => "Last Quarter",
}
}
}
#[pyclass(name = "PhaseEvent", module = "siderust", from_py_object)]
#[derive(Clone)]
pub struct PyPhaseEvent {
pub mjd: f64,
pub kind: PyPhaseKind,
}
impl From<lunar::PhaseEvent> for PyPhaseEvent {
fn from(e: lunar::PhaseEvent) -> Self {
Self {
mjd: e.mjd.value(),
kind: e.kind.into(),
}
}
}
#[pymethods]
impl PyPhaseEvent {
#[getter]
fn mjd(&self) -> f64 {
self.mjd
}
#[getter]
fn kind(&self) -> PyPhaseKind {
self.kind
}
fn __repr__(&self) -> String {
format!("PhaseEvent(mjd={:.6}, kind={:?})", self.mjd, self.kind)
}
fn __str__(&self) -> String {
format!("MJD {:.6}: {}", self.mjd, self.kind.__str__())
}
}
#[pyfunction]
#[pyo3(signature = (jd, observer = None))]
pub fn moon_phase(jd: f64, observer: Option<&PyObserver>) -> PyMoonPhaseGeometry {
let jd = JulianDate::new(jd);
let geom = match observer {
Some(obs) => lunar::moon_phase_topocentric::<Vsop87Ephemeris>(jd, obs.inner),
None => lunar::moon_phase_geocentric::<Vsop87Ephemeris>(jd),
};
geom.into()
}
#[pyfunction]
pub fn find_moon_phases(start_mjd: f64, end_mjd: f64) -> PyResult<Vec<PyPhaseEvent>> {
if start_mjd >= end_mjd {
return Err(invalid_period_error());
}
let window = Interval::new(
ModifiedJulianDate::new(start_mjd),
ModifiedJulianDate::new(end_mjd),
);
let events = lunar::find_phase_events::<Vsop87Ephemeris>(window, Default::default());
Ok(events.into_iter().map(PyPhaseEvent::from).collect())
}