use pyo3::exceptions::PyImportError;
use pyo3::prelude::*;
use pyo3::types::PyModule;
use siderust::coordinates::centers::Geodetic;
use siderust::coordinates::frames::ECEF;
use siderust::coordinates::spherical::direction;
use siderust::qtty::{Degrees, Meters};
const EXTENSION_MODULE: &str = "siderust._siderust";
const BRIDGE_PROTOCOL_ATTRIBUTE: &str = "_bridge_protocol_version";
const OBSERVER_FROM_PARTS: &str = "_bridge_observer_from_parts";
const OBSERVER_TO_PARTS: &str = "_bridge_observer_to_parts";
const DIRECTION_FROM_PARTS: &str = "_bridge_direction_from_parts";
const DIRECTION_TO_PARTS: &str = "_bridge_direction_to_parts";
pub const BRIDGE_PROTOCOL_VERSION: u32 = 1;
fn bridge_module<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyModule>> {
let module = PyModule::import(py, EXTENSION_MODULE)?;
let actual = module
.getattr(BRIDGE_PROTOCOL_ATTRIBUTE)
.and_then(|value| value.extract::<u32>())
.map_err(|_| {
PyImportError::new_err(format!(
"installed {EXTENSION_MODULE} does not expose a valid \
{BRIDGE_PROTOCOL_ATTRIBUTE}; expected bridge protocol \
{BRIDGE_PROTOCOL_VERSION}. Install a compatible siderust Python package"
))
})?;
if actual != BRIDGE_PROTOCOL_VERSION {
return Err(PyImportError::new_err(format!(
"incompatible siderust bridge protocol: expected \
{BRIDGE_PROTOCOL_VERSION}, found {actual}. Install matching \
siderust-py Rust and Python package versions"
)));
}
Ok(module)
}
pub fn ensure_bridge_protocol(py: Python<'_>) -> PyResult<()> {
bridge_module(py).map(|_| ())
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ObserverParts {
pub longitude_degrees: f64,
pub latitude_degrees: f64,
pub height_metres: f64,
}
impl ObserverParts {
pub fn into_observer(self) -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(
Degrees::new(self.longitude_degrees),
Degrees::new(self.latitude_degrees),
Meters::new(self.height_metres),
)
}
}
impl From<&Geodetic<ECEF>> for ObserverParts {
fn from(observer: &Geodetic<ECEF>) -> Self {
Self {
longitude_degrees: observer.lon.value(),
latitude_degrees: observer.lat.value(),
height_metres: observer.height.value(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DirectionParts {
pub right_ascension_degrees: f64,
pub declination_degrees: f64,
}
impl DirectionParts {
pub fn into_direction(self) -> direction::ICRS {
direction::ICRS::new(
Degrees::new(self.right_ascension_degrees),
Degrees::new(self.declination_degrees),
)
}
}
impl From<&direction::ICRS> for DirectionParts {
fn from(value: &direction::ICRS) -> Self {
Self {
right_ascension_degrees: value.azimuth.value(),
declination_degrees: value.polar.value(),
}
}
}
pub fn observer_parts_from_python(value: &Bound<'_, PyAny>) -> PyResult<ObserverParts> {
let (longitude_degrees, latitude_degrees, height_metres): (f64, f64, f64) =
bridge_module(value.py())?
.getattr(OBSERVER_TO_PARTS)?
.call1((value,))?
.extract()?;
Ok(ObserverParts {
longitude_degrees,
latitude_degrees,
height_metres,
})
}
pub fn observer_from_python(value: &Bound<'_, PyAny>) -> PyResult<Geodetic<ECEF>> {
Ok(observer_parts_from_python(value)?.into_observer())
}
pub fn observer_to_python(py: Python<'_>, observer: &Geodetic<ECEF>) -> PyResult<Py<PyAny>> {
let parts = ObserverParts::from(observer);
bridge_module(py)?
.getattr(OBSERVER_FROM_PARTS)?
.call1((
parts.longitude_degrees,
parts.latitude_degrees,
parts.height_metres,
))
.map(Bound::unbind)
}
pub fn direction_parts_from_python(value: &Bound<'_, PyAny>) -> PyResult<DirectionParts> {
let (right_ascension_degrees, declination_degrees): (f64, f64) = bridge_module(value.py())?
.getattr(DIRECTION_TO_PARTS)?
.call1((value,))?
.extract()?;
Ok(DirectionParts {
right_ascension_degrees,
declination_degrees,
})
}
pub fn direction_from_python(value: &Bound<'_, PyAny>) -> PyResult<direction::ICRS> {
Ok(direction_parts_from_python(value)?.into_direction())
}
pub fn direction_to_python(py: Python<'_>, value: &direction::ICRS) -> PyResult<Py<PyAny>> {
let parts = DirectionParts::from(value);
bridge_module(py)?
.getattr(DIRECTION_FROM_PARTS)?
.call1((parts.right_ascension_degrees, parts.declination_degrees))
.map(Bound::unbind)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observer_parts_round_trip() {
let parts = ObserverParts {
longitude_degrees: -17.8925,
latitude_degrees: 28.7543,
height_metres: 2396.0,
};
let round_trip = ObserverParts::from(&parts.into_observer());
assert!((round_trip.longitude_degrees - parts.longitude_degrees).abs() < 1e-12);
assert!((round_trip.latitude_degrees - parts.latitude_degrees).abs() < 1e-12);
assert_eq!(round_trip.height_metres, parts.height_metres);
}
#[test]
fn direction_parts_round_trip() {
let parts = DirectionParts {
right_ascension_degrees: 83.633,
declination_degrees: 22.014,
};
let round_trip = DirectionParts::from(&parts.into_direction());
assert!((round_trip.right_ascension_degrees - parts.right_ascension_degrees).abs() < 1e-12);
assert!((round_trip.declination_degrees - parts.declination_degrees).abs() < 1e-12);
}
}