diff --git a/CHANGELOG.md b/CHANGELOG.md index ed810c7..447c934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the JavaScript workspace are documented here. +## [Unreleased] + +### Changed +- Documented the shared `rework-ffi` development flow for `qtty-js`, + `tempoch-js`, and `siderust-js`, including the canonical-workspace path + patches used for local integration and the branch-pinned git-patch workflow + to use once those Rust branches are committed. + ## [0.1.0] - 2026-03-11 ### Added diff --git a/README.md b/README.md index 2ebdfec..320559a 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,20 @@ wasm-pack build --target web --out-dir pkg --release --scope siderust npm test ``` +### Canonical Rust Sync + +The Rust crates behind `siderust-js` are intentionally patched to the canonical +workspace checkouts under `../../../rust/`. Keep `qtty-js`, `tempoch-js`, and +`siderust-js` aligned on the shared `rework-ffi` branch so the Node and Web +packages see the same `qtty`, `tempoch`, and `siderust` behavior during FFI +consolidation. + +For local iteration, the current path patches are the safest option because +they follow uncommitted workspace changes immediately. If you need branch-pinned +Cargo resolution for CI or a separate clone, switch those patches to local +`git = "file:///..."` URLs on `branch = "rework-ffi"` after committing the Rust +workspaces. + ## Related documentation - [`siderust-node/README.md`](./siderust-node/README.md) for the Node package diff --git a/qtty-js b/qtty-js index c13e25c..355873a 160000 --- a/qtty-js +++ b/qtty-js @@ -1 +1 @@ -Subproject commit c13e25c809fe6e8af02ecb92c4d6c9155b0246c8 +Subproject commit 355873ad5e9167bfa386195a0ac353d7abdd4056 diff --git a/siderust-core/Cargo.lock b/siderust-core/Cargo.lock index cd87ca6..d017a74 100644 --- a/siderust-core/Cargo.lock +++ b/siderust-core/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "affn" -version = "0.3.3" +version = "0.5.0" dependencies = [ "affn-derive", "nalgebra", - "qtty", + "qtty 0.4.1", ] [[package]] @@ -90,8 +90,10 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cheby" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af87a2f4182a67a08e5618b52859ef58cca770526061f0f43eaa754f12ecd30" dependencies = [ - "qtty", + "qtty 0.3.1", ] [[package]] @@ -397,30 +399,56 @@ dependencies = [ [[package]] name = "qtty" -version = "0.4.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea89a195c12cff1a98730ebdbf28ac603d75a5eaba14e0b0d0704535f67a811" +checksum = "b42948ee9aba0ed2217fd0d89a666df2ab020e3e25ef9b0328150e6d6c0aaee3" +dependencies = [ + "qtty-core 0.3.1", + "qtty-derive 0.3.1", +] + +[[package]] +name = "qtty" +version = "0.4.1" dependencies = [ - "qtty-core", - "qtty-derive", + "qtty-core 0.4.1", + "qtty-derive 0.4.1", ] [[package]] name = "qtty-core" -version = "0.4.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec18c85c65728c91aeec04456140bece6d71cb703307f786303523957759718" +checksum = "283c549662e44f8a2848964e9639f2924f7a442f94294d508fc70c531a9717b3" dependencies = [ "libm", - "qtty-derive", + "qtty-derive 0.3.1", + "typenum", +] + +[[package]] +name = "qtty-core" +version = "0.4.1" +dependencies = [ + "libm", + "qtty-derive 0.4.1", "typenum", ] [[package]] name = "qtty-derive" -version = "0.4.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c4ed7c0500072e756d76102cf8fceebe6ae9199e6e16ca018948eb80c78625" +checksum = "5edb55d59461af47a984baae7fafc2374dac2a79d41434a77ee7807f0f592757" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "qtty-derive" +version = "0.4.1" dependencies = [ "proc-macro2", "quote", @@ -512,7 +540,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siderust" -version = "0.5.2" +version = "0.6.0" dependencies = [ "affn", "anyhow", @@ -521,11 +549,11 @@ dependencies = [ "chrono", "nalgebra", "paste", - "qtty", + "qtty 0.4.1", "regex", "tempoch", "walkdir", - "wide 1.1.1", + "wide 1.2.0", ] [[package]] @@ -534,7 +562,7 @@ version = "0.1.0" dependencies = [ "affn", "chrono", - "qtty", + "qtty 0.4.1", "siderust", "tempoch", ] @@ -565,17 +593,17 @@ dependencies = [ [[package]] name = "tempoch" -version = "0.3.0" +version = "0.4.0" dependencies = [ "tempoch-core", ] [[package]] name = "tempoch-core" -version = "0.3.0" +version = "0.4.0" dependencies = [ "chrono", - "qtty", + "qtty 0.4.1", ] [[package]] @@ -657,9 +685,9 @@ dependencies = [ [[package]] name = "wide" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac11b009ebeae802ed758530b6496784ebfee7a87b9abfbcaf3bbe25b814eb25" +checksum = "198f6abc41fab83526d10880fa5c17e2b4ee44e763949b4bb34e2fd1e8ca48e4" dependencies = [ "bytemuck", "safe_arch 1.0.0", diff --git a/siderust-core/Cargo.toml b/siderust-core/Cargo.toml index 8d5f71f..83b4afb 100644 --- a/siderust-core/Cargo.toml +++ b/siderust-core/Cargo.toml @@ -14,5 +14,11 @@ name = "siderust_binding_core" siderust = { version = "0.6.0", features = [] } qtty = "0.4.1" tempoch = "0.4" -affn = { version = "0.4.1", features = ["astro"] } +affn = { version = "0.5.0", features = ["astro"] } chrono = "0.4.44" + +[patch.crates-io] +affn = { path = "../../../rust/affn" } +qtty = { path = "../../../rust/qtty/qtty" } +siderust = { path = "../../../rust/siderust" } +tempoch = { path = "../../../rust/tempoch/tempoch" } diff --git a/siderust-core/src/body.rs b/siderust-core/src/body.rs index 95b8284..f865921 100644 --- a/siderust-core/src/body.rs +++ b/siderust-core/src/body.rs @@ -97,9 +97,9 @@ impl PlanetInfo { name: name.to_string(), mass_kg: p.mass.value(), radius_km: p.radius.value(), - semi_major_axis_au: p.orbit.semi_major_axis.value(), - eccentricity: p.orbit.eccentricity, - inclination_deg: p.orbit.inclination.value(), + semi_major_axis_au: p.orbit.shape().semi_major_axis().value(), + eccentricity: p.orbit.shape().eccentricity(), + inclination_deg: p.orbit.orientation().inclination().value(), } } } diff --git a/siderust-core/src/events.rs b/siderust-core/src/events.rs index bec057a..5f41a8e 100644 --- a/siderust-core/src/events.rs +++ b/siderust-core/src/events.rs @@ -106,6 +106,16 @@ use siderust::calculus::azimuth::{ AzimuthCrossingEvent, AzimuthExtremum, AzimuthExtremumKind, }; use tempoch::{Period, MJD}; +use qtty::*; +use siderust::calculus::altitude::{self, SearchOpts}; +use siderust::calculus::azimuth; +use siderust::coordinates::centers::Geodetic; +use siderust::coordinates::frames::ECEF; +use siderust::AltitudePeriodsProvider; +use siderust::AzimuthProvider; + +use crate::body::BodyKind; +use crate::dispatch_body; impl From for CrossingDirection { fn from(d: SiderustCrossingDirection) -> Self { @@ -182,3 +192,263 @@ impl From for AzimuthExtremumData { } } } + +fn validate_finite(label: &str, value: f64) -> Result { + if value.is_finite() { + Ok(value) + } else { + Err(format!("{label} must be finite")) + } +} + +pub fn make_window(start_mjd: f64, end_mjd: f64) -> Result, String> { + validate_finite("startMjd", start_mjd)?; + validate_finite("endMjd", end_mjd)?; + if start_mjd >= end_mjd { + return Err("Window start must be before end (startMjd < endMjd)".to_string()); + } + Ok(Period::new( + tempoch::ModifiedJulianDate::new(start_mjd), + tempoch::ModifiedJulianDate::new(end_mjd), + )) +} + +pub fn body_altitude_at( + body: BodyKind, + observer: &Geodetic, + mjd: f64, +) -> Result { + let mjd = validate_finite("mjd", mjd)?; + let instant = tempoch::ModifiedJulianDate::new(mjd); + Ok(dispatch_body!(body, |b| { + b.altitude_at(observer, instant).to::().value() + })) +} + +pub fn body_azimuth_at( + body: BodyKind, + observer: &Geodetic, + mjd: f64, +) -> Result { + let mjd = validate_finite("mjd", mjd)?; + let instant = tempoch::ModifiedJulianDate::new(mjd); + Ok(dispatch_body!(body, |b| { + b.azimuth_at(observer, instant).to::().value() + })) +} + +pub fn body_crossings( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + altitude::crossings(&b, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn body_culminations( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + altitude::culminations(&b, observer, window, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn body_above_threshold( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + altitude::above_threshold(&b, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn body_below_threshold( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + altitude::below_threshold(&b, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn body_azimuth_crossings( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + azimuth_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let bearing = Degrees::new(validate_finite("azimuthDeg", azimuth_deg)?); + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + azimuth::azimuth_crossings(&b, observer, window, bearing, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn body_azimuth_extrema( + body: BodyKind, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let opts = SearchOpts::default(); + Ok(dispatch_body!(body, |b| { + azimuth::azimuth_extrema(&b, observer, window, opts) + .into_iter() + .map(Into::into) + .collect() + })) +} + +pub fn star_altitude_at( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + mjd: f64, +) -> Result { + let mjd = validate_finite("mjd", mjd)?; + let instant = tempoch::ModifiedJulianDate::new(mjd); + Ok(star.altitude_at(observer, instant).to::().value()) +} + +pub fn star_azimuth_at( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + mjd: f64, +) -> Result { + let mjd = validate_finite("mjd", mjd)?; + let instant = tempoch::ModifiedJulianDate::new(mjd); + Ok(star.azimuth_at(observer, instant).to::().value()) +} + +pub fn star_crossings( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(altitude::crossings(star, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect()) +} + +pub fn star_culminations( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let opts = SearchOpts::default(); + Ok(altitude::culminations(star, observer, window, opts) + .into_iter() + .map(Into::into) + .collect()) +} + +pub fn star_above_threshold( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(altitude::above_threshold(star, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect()) +} + +pub fn star_below_threshold( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + threshold_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let threshold = Degrees::new(validate_finite("thresholdDeg", threshold_deg)?); + let opts = SearchOpts::default(); + Ok(altitude::below_threshold(star, observer, window, threshold, opts) + .into_iter() + .map(Into::into) + .collect()) +} + +pub fn star_azimuth_crossings( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, + azimuth_deg: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let bearing = Degrees::new(validate_finite("azimuthDeg", azimuth_deg)?); + let opts = SearchOpts::default(); + Ok(azimuth::azimuth_crossings(star, observer, window, bearing, opts) + .into_iter() + .map(Into::into) + .collect()) +} + +pub fn star_azimuth_extrema( + star: &siderust::bodies::Star<'_>, + observer: &Geodetic, + start_mjd: f64, + end_mjd: f64, +) -> Result, String> { + let window = make_window(start_mjd, end_mjd)?; + let opts = SearchOpts::default(); + Ok(azimuth::azimuth_extrema(star, observer, window, opts) + .into_iter() + .map(Into::into) + .collect()) +} diff --git a/siderust-core/src/lib.rs b/siderust-core/src/lib.rs index 18f751f..b30ecff 100644 --- a/siderust-core/src/lib.rs +++ b/siderust-core/src/lib.rs @@ -18,3 +18,4 @@ pub mod body; pub mod events; pub mod observer; pub mod position; +pub mod star; diff --git a/siderust-core/src/observer.rs b/siderust-core/src/observer.rs index d9ee1ee..30c2a75 100644 --- a/siderust-core/src/observer.rs +++ b/siderust-core/src/observer.rs @@ -41,6 +41,11 @@ pub fn mauna_kea() -> Geodetic { observatories::MAUNA_KEA } +/// Get La Silla observatory. +pub fn la_silla() -> Geodetic { + observatories::LA_SILLA_OBSERVATORY +} + /// Get observer property values. pub fn get_observer_lon_deg(obs: &Geodetic) -> f64 { obs.lon.to::().value() diff --git a/siderust-core/src/star.rs b/siderust-core/src/star.rs new file mode 100644 index 0000000..1f52ce4 --- /dev/null +++ b/siderust-core/src/star.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Star validation and catalog helpers shared between Node and Web bindings. + +use std::borrow::Cow; + +use qtty::length::nominal::SolarRadiuses; +use qtty::*; +use siderust::bodies::{self, Star as SiderustStar}; +use siderust::coordinates::centers::Geocentric; +use siderust::coordinates::frames::EquatorialMeanJ2000; +use siderust::coordinates::spherical; +use siderust::targets::CoordinateWithPM; +use tempoch::JulianDate; + +pub const CATALOG_STAR_NAMES: &[&str] = &[ + "Vega", + "Sirius", + "Polaris", + "Canopus", + "Arcturus", + "Rigel", + "Betelgeuse", + "Procyon", + "Aldebaran", + "Altair", +]; + +pub fn validate_star_params( + distance_ly: f64, + mass_solar: f64, + radius_solar: f64, + luminosity_solar: f64, + ra_deg: f64, + dec_deg: f64, +) -> Result<(), String> { + for (label, value) in [ + ("distanceLy", distance_ly), + ("massSolar", mass_solar), + ("radiusSolar", radius_solar), + ("luminositySolar", luminosity_solar), + ("raDeg", ra_deg), + ("decDeg", dec_deg), + ] { + if !value.is_finite() { + return Err(format!("{label} must be finite (not NaN or ±infinity)")); + } + } + Ok(()) +} + +pub fn create_star( + name: impl Into, + distance_ly: f64, + mass_solar: f64, + radius_solar: f64, + luminosity_solar: f64, + ra_deg: f64, + dec_deg: f64, +) -> Result, String> { + validate_star_params( + distance_ly, + mass_solar, + radius_solar, + luminosity_solar, + ra_deg, + dec_deg, + )?; + + let pos = spherical::Position::::new( + Degrees::new(ra_deg), + Degrees::new(dec_deg), + LightYears::new(distance_ly), + ); + let target = CoordinateWithPM::< + spherical::Position, + >::new_static(pos, JulianDate::J2000); + + Ok(SiderustStar::new( + Cow::<'static, str>::Owned(name.into()), + LightYears::new(distance_ly), + SolarMasses::new(mass_solar), + SolarRadiuses::new(radius_solar), + SolarLuminosities::new(luminosity_solar), + target, + )) +} + +pub fn catalog_star(name: &str) -> Result, String> { + let star: &SiderustStar<'static> = match name.to_uppercase().as_str() { + "VEGA" => &bodies::VEGA, + "SIRIUS" => &bodies::SIRIUS, + "POLARIS" => &bodies::POLARIS, + "CANOPUS" => &bodies::CANOPUS, + "ARCTURUS" => &bodies::ARCTURUS, + "RIGEL" => &bodies::RIGEL, + "BETELGEUSE" => &bodies::BETELGEUSE, + "PROCYON" => &bodies::PROCYON, + "ALDEBARAN" => &bodies::ALDEBARAN, + "ALTAIR" => &bodies::ALTAIR, + _ => { + return Err(format!( + "Unknown catalog star: \"{name}\". Available: {}.", + CATALOG_STAR_NAMES.join(", ") + )); + } + }; + Ok(star.clone()) +} + +pub fn list_catalog_star_names() -> Vec { + CATALOG_STAR_NAMES.iter().map(|name| (*name).to_string()).collect() +} diff --git a/siderust-node/Cargo.lock b/siderust-node/Cargo.lock index 45e2c00..e259737 100644 --- a/siderust-node/Cargo.lock +++ b/siderust-node/Cargo.lock @@ -4,9 +4,7 @@ version = 4 [[package]] name = "affn" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6639fb5f399ec93eceeec24afc3608b9760ddcdece6936412d04ab478627d2" +version = "0.5.0" dependencies = [ "affn-derive", "nalgebra", @@ -16,8 +14,6 @@ dependencies = [ [[package]] name = "affn-derive" version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20fe1fdcdc9c8b7fa557c99dc1acacbc90d7c64c8d2826e83f01e414fca1cf18" dependencies = [ "proc-macro2", "quote", @@ -506,8 +502,6 @@ dependencies = [ [[package]] name = "qtty" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dc60ed0161fc68ec0b0faf090e4feec70dd179447c6ca14e5cc052b4eb2a789" dependencies = [ "qtty-core 0.4.1", "qtty-derive 0.4.1", @@ -527,8 +521,6 @@ dependencies = [ [[package]] name = "qtty-core" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab488371c17ea2db038caa2a3d0044a85a433c4c2172467978d05ee39d22b6fb" dependencies = [ "libm", "qtty-derive 0.4.1", @@ -549,8 +541,6 @@ dependencies = [ [[package]] name = "qtty-derive" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab6c550316cbdd43e4eca2c94c14c1cf4e67b253278a94ccc4a5eca2ce5db6e" dependencies = [ "proc-macro2", "quote", @@ -649,8 +639,6 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siderust" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9322877f7730d54aef3db9f91f73b918c82d7ffe950ef3272a7f6d40b8e469dc" dependencies = [ "affn", "anyhow", @@ -663,7 +651,7 @@ dependencies = [ "regex", "tempoch", "walkdir", - "wide 1.1.1", + "wide 1.2.0", ] [[package]] @@ -719,8 +707,6 @@ dependencies = [ [[package]] name = "tempoch" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acec780480096b36a1872a5131de3e90463083d0a5c5bd13779defc18a530384" dependencies = [ "tempoch-core", ] @@ -728,8 +714,6 @@ dependencies = [ [[package]] name = "tempoch-core" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3f7d4b6138b4144837ed3d38042737f16ce3d909661f193fdaf3f40faf7422" dependencies = [ "chrono", "qtty 0.4.1", @@ -820,9 +804,9 @@ dependencies = [ [[package]] name = "wide" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac11b009ebeae802ed758530b6496784ebfee7a87b9abfbcaf3bbe25b814eb25" +checksum = "198f6abc41fab83526d10880fa5c17e2b4ee44e763949b4bb34e2fd1e8ca48e4" dependencies = [ "bytemuck", "safe_arch 1.0.0", diff --git a/siderust-node/Cargo.toml b/siderust-node/Cargo.toml index a938a13..0b7cffd 100644 --- a/siderust-node/Cargo.toml +++ b/siderust-node/Cargo.toml @@ -16,7 +16,7 @@ siderust = { version = "0.6.0", features = [] } siderust-binding-core = { path = "../siderust-core" } qtty = "0.4.1" tempoch = "0.4" -affn = { version = "0.4.1", features = ["astro"] } +affn = { version = "0.5.0", features = ["astro"] } chrono = "0.4.44" napi = { version = "2", features = ["napi6"] } napi-derive = "2" @@ -27,3 +27,9 @@ napi-build = "2" [profile.release] lto = true strip = "symbols" + +[patch.crates-io] +affn = { path = "../../../rust/affn" } +qtty = { path = "../../../rust/qtty/qtty" } +siderust = { path = "../../../rust/siderust" } +tempoch = { path = "../../../rust/tempoch/tempoch" } diff --git a/siderust-node/index.d.ts b/siderust-node/index.d.ts index 3aa452e..e69de29 100644 --- a/siderust-node/index.d.ts +++ b/siderust-node/index.d.ts @@ -1,623 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ - -/* auto-generated by NAPI-RS */ - -/** Physical parameters of a solar-system planet. */ -export interface PlanetInfo { - /** Planet name. */ - name: string - /** Mass in kilograms. */ - massKg: number - /** Mean equatorial radius in kilometres. */ - radiusKm: number - /** Semi-major axis in AU. */ - semiMajorAxisAu: number - /** Orbital eccentricity. */ - eccentricity: number - /** Orbital inclination in degrees. */ - inclinationDeg: number -} -/** - * Get physical parameters for a named planet. - * - * Valid names: `"Mercury"`, `"Venus"`, `"Earth"`, `"Mars"`, `"Jupiter"`, - * `"Saturn"`, `"Uranus"`, `"Neptune"`. - * - * ```js - * const { getPlanet } = require('@siderust/siderust'); - * const mars = getPlanet('Mars'); - * console.log(mars.massKg, mars.radiusKm); - * ``` - */ -export declare function getPlanet(name: string): PlanetInfo -/** List the names of all available solar-system bodies for altitude/azimuth queries. */ -export declare function listBodies(): Array -/** A spherical direction (two angles in degrees) with a frame label. */ -export interface SphericalDirection { - /** First angle in degrees (Dec / polar / altitude). */ - polarDeg: number - /** Second angle in degrees (RA / azimuth / longitude). */ - azimuthDeg: number - /** Reference frame name. */ - frame: string -} -/** Cartesian ECEF coordinates in metres. */ -export interface CartesianEcef { - /** X coordinate in metres. */ - x: number - /** Y coordinate in metres. */ - y: number - /** Z coordinate in metres. */ - z: number -} -/** - * Transform a spherical direction between celestial reference frames. - * - * Supported source and destination frames: `"ICRS"`, `"EclipticMeanJ2000"`, - * `"EquatorialMeanJ2000"`, `"EquatorialMeanOfDate"`, `"EquatorialTrueOfDate"`. - * - * @param polarDeg — Declination / polar angle in degrees. - * @param azimuthDeg — Right ascension / longitude in degrees. - * @param srcFrame — Source frame name. - * @param dstFrame — Destination frame name. - * @param jd — Julian Date (needed for of-date frames; use J2000 = 2451545.0 for epoch-independent frames). - * - * ```js - * const { transformDirection } = require('@siderust/siderust'); - * const result = transformDirection(38.78, 279.23, 'EquatorialMeanJ2000', 'EclipticMeanJ2000', 2451545.0); - * ``` - */ -export declare function transformDirection(polarDeg: number, azimuthDeg: number, srcFrame: string, dstFrame: string, jd: number): SphericalDirection -/** - * Transform a spherical direction to the local Horizontal frame. - * - * @returns `{ polarDeg, azimuthDeg, frame }` where polarDeg is altitude - * and azimuthDeg is azimuth (N=0° E=90°). - */ -export declare function directionToHorizontal(polarDeg: number, azimuthDeg: number, srcFrame: string, jd: number, observer: JsObserver): SphericalDirection -/** - * Convert a geodetic position (WGS84) to ECEF Cartesian coordinates. - * - * @param observer — Observer with lon/lat/height. - * @returns `{ x, y, z }` in metres. - */ -export declare function geodeticToEcef(observer: JsObserver): CartesianEcef -/** - * Compute the angular separation between two spherical directions (Vincenty formula). - * - * Both directions must be in the same reference frame. - * - * @param polar1Deg — Declination / polar angle of first direction in degrees. - * @param azimuth1Deg — RA / azimuth of first direction in degrees. - * @param polar2Deg — Declination / polar angle of second direction in degrees. - * @param azimuth2Deg — RA / azimuth of second direction in degrees. - * @param frame — Reference frame (both directions must share the same frame). - * @returns Angular separation in degrees. - * - * ```js - * const { angularSeparation } = require('@siderust/siderust'); - * const sep = angularSeparation(89.26, 37.95, -16.72, 101.29, 'EquatorialMeanJ2000'); - * ``` - */ -export declare function angularSeparation(polar1Deg: number, azimuth1Deg: number, polar2Deg: number, azimuth2Deg: number, frame: string): number -/** - * Compute the Euclidean distance between two 3D Cartesian positions. - * - * The positions must be in the same frame and center. Units follow input. - * - * @returns Distance in the same units as the input coordinates. - */ -export declare function cartesianDistance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number -/** - * Compute the magnitude (distance from origin) of a 3D Cartesian vector. - * - * @returns Distance from origin in the same units. - */ -export declare function cartesianMagnitude(x: number, y: number, z: number): number -/** - * Compute the dot product of two 3D Cartesian vectors. - * - * @returns The scalar dot product. - */ -export declare function dotProduct(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number -/** - * Convert a direction (unit vector) from spherical to Cartesian. - * - * @param polarDeg — Declination / polar angle in degrees. - * @param azimuthDeg — RA / azimuth in degrees. - * @returns `{ x, y, z }` unit vector. - */ -export declare function directionToCartesian(polarDeg: number, azimuthDeg: number): CartesianEcef -/** A 3D Cartesian position with frame and center metadata. */ -export interface CartesianPosition { - /** X coordinate (AU for solar-system, km for Moon geocentric). */ - x: number - /** Y coordinate. */ - y: number - /** Z coordinate. */ - z: number - /** Reference frame (e.g. `"EclipticMeanJ2000"`). */ - frame: string - /** Reference center (e.g. `"Heliocentric"`, `"Barycentric"`, `"Geocentric"`). */ - center: string -} -/** - * Compute the heliocentric ecliptic position of a planet via VSOP87 (Series A). - * - * Returns Cartesian coordinates in AU, in the EclipticMeanJ2000 frame, - * centered on the Sun. - * - * @param body — Planet name: Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune. - * @param jd — Julian Date of the epoch. - * - * ```js - * const { vsop87Heliocentric } = require('@siderust/siderust'); - * const mars = vsop87Heliocentric('Mars', 2451545.0); - * console.log(mars.x, mars.y, mars.z); - * ``` - */ -export declare function vsop87Heliocentric(body: string, jd: number): CartesianPosition -/** - * Compute the barycentric ecliptic position of a planet via VSOP87 (Series E). - * - * Returns Cartesian coordinates in AU, in the EclipticMeanJ2000 frame, - * centered on the solar-system barycentre. - * - * @param body — Planet name or `"Sun"`. - * @param jd — Julian Date of the epoch. - */ -export declare function vsop87Barycentric(body: string, jd: number): CartesianPosition -/** - * Get the Sun's barycentric position via VSOP87. - * - * @param jd — Julian Date. - * @returns Cartesian position in AU (EclipticMeanJ2000, Barycentric). - */ -export declare function vsop87SunBarycentric(jd: number): CartesianPosition -/** - * Get the Earth's barycentric position via VSOP87. - * - * @param jd — Julian Date. - * @returns Cartesian position in AU (EclipticMeanJ2000, Barycentric). - */ -export declare function vsop87EarthBarycentric(jd: number): CartesianPosition -/** - * Get the Earth's heliocentric position via VSOP87. - * - * @param jd — Julian Date. - * @returns Cartesian position in AU (EclipticMeanJ2000, Heliocentric). - */ -export declare function vsop87EarthHeliocentric(jd: number): CartesianPosition -/** - * Get the Moon's geocentric position via ELP2000. - * - * @param jd — Julian Date. - * @returns Cartesian position in **km** (EclipticMeanJ2000, Geocentric). - */ -export declare function vsop87MoonGeocentric(jd: number): CartesianPosition -/** - * Transform a Cartesian position from one reference center to another. - * - * Stays in the EclipticMeanJ2000 frame (the natural VSOP87 frame). - * Units are AU (except Geocentric Moon which is km). - * - * Supported center pairs: Heliocentric ↔ Geocentric, Heliocentric ↔ Barycentric, - * Geocentric ↔ Barycentric. - * - * @param x, y, z — Cartesian coordinates. - * @param srcCenter — Source center: `"Heliocentric"`, `"Barycentric"`, `"Geocentric"`. - * @param dstCenter — Destination center. - * @param jd — Julian Date. - * - * ```js - * const mars = vsop87Heliocentric('Mars', 2451545.0); - * const marsGeo = transformPositionCenter(mars.x, mars.y, mars.z, 'Heliocentric', 'Geocentric', 2451545.0); - * ``` - */ -export declare function transformPositionCenter(x: number, y: number, z: number, srcCenter: string, dstCenter: string, jd: number): CartesianPosition -/** - * Transform a Cartesian position between celestial reference frames. - * - * Stays in the same center. Coordinates are in AU. - * - * Supported frames: `"ICRS"`, `"EclipticMeanJ2000"`, `"EquatorialMeanJ2000"`. - * - * @param x, y, z — Cartesian coordinates in AU. - * @param srcFrame — Source frame name. - * @param dstFrame — Destination frame name. - * @param jd — Julian Date. - */ -export declare function transformPositionFrame(x: number, y: number, z: number, srcFrame: string, dstFrame: string, jd: number): CartesianPosition -/** - * Compute the orbital period of a named planet in days (Keplerian). - * - * @param name — Planet name. - * @returns Orbital period in days. - */ -export declare function orbitalPeriodDays(name: string): number -/** A threshold-crossing event (rise or set). */ -export interface CrossingEvent { - /** Time of the crossing (Modified Julian Date). */ - mjd: number - /** Direction: `"rising"` or `"setting"`. */ - direction: string -} -/** A culmination event (local altitude extremum). */ -export interface CulminationEvent { - /** Time of the culmination (Modified Julian Date). */ - mjd: number - /** Altitude at the extremum in degrees. */ - altitudeDeg: number - /** Kind: `"max"` (upper culmination) or `"min"` (lower culmination). */ - kind: string -} -/** A time period (MJD interval). */ -export interface MjdPeriod { - /** Start of the period (Modified Julian Date). */ - startMjd: number - /** End of the period (Modified Julian Date). */ - endMjd: number -} -/** An azimuth-crossing event. */ -export interface AzimuthCrossingEvent { - /** Time of the event (Modified Julian Date). */ - mjd: number - /** Crossing direction: `"rising"` or `"setting"`. */ - direction: string -} -/** An azimuth extremum (local max or min bearing). */ -export interface AzimuthExtremum { - /** Time of the extremum (Modified Julian Date). */ - mjd: number - /** Azimuth at the extremum in degrees. */ - azimuthDeg: number - /** Kind: `"max"` or `"min"`. */ - kind: string -} -/** - * Compute the altitude of a solar-system body at a single instant. - * - * @param body — Body name (e.g. `"Sun"`, `"Moon"`, `"Mars"`). - * @param observer — Observer location. - * @param mjd — Modified Julian Date of the instant. - * @returns Altitude in degrees. - * - * ```js - * const { Observer, bodyAltitudeAt } = require('@siderust/siderust'); - * const obs = Observer.roqueDeLasMuchachos(); - * const alt = bodyAltitudeAt('Sun', obs, 60000.0); - * ``` - */ -export declare function bodyAltitudeAt(body: string, observer: JsObserver, mjd: number): number -/** - * Compute the azimuth of a solar-system body at a single instant. - * - * @returns Azimuth in degrees (north = 0°, east = 90°). - */ -export declare function bodyAzimuthAt(body: string, observer: JsObserver, mjd: number): number -/** - * Find threshold-crossing events (rise/set) for a solar-system body. - * - * @param body — Body name. - * @param observer — Observer location. - * @param startMjd — Window start (MJD). - * @param endMjd — Window end (MJD). - * @param thresholdDeg — Altitude threshold in degrees (e.g. 0 for horizon). - * @returns Array of crossing events `{ mjd, direction }`. - */ -export declare function bodyCrossings(body: string, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** - * Find culmination events (altitude local extrema) for a solar-system body. - * - * @returns Array of culmination events `{ mjd, altitudeDeg, kind }`. - */ -export declare function bodyCulminations(body: string, observer: JsObserver, startMjd: number, endMjd: number): Array -/** - * Find periods where a body's altitude is above a threshold. - * - * @returns Array of MJD periods `{ startMjd, endMjd }`. - */ -export declare function bodyAboveThreshold(body: string, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** - * Find periods where a body's altitude is below a threshold. - * - * @returns Array of MJD periods `{ startMjd, endMjd }`. - */ -export declare function bodyBelowThreshold(body: string, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** - * Find azimuth-crossing events for a body. - * - * @param bearingDeg — Target azimuth bearing in degrees. - * @returns Array of azimuth crossing events `{ mjd, direction }`. - */ -export declare function bodyAzimuthCrossings(body: string, observer: JsObserver, startMjd: number, endMjd: number, bearingDeg: number): Array -/** - * Find azimuth extrema (max/min bearing) for a body. - * - * @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`. - */ -export declare function bodyAzimuthExtrema(body: string, observer: JsObserver, startMjd: number, endMjd: number): Array -/** - * Compute the altitude of a catalog or custom star at a single instant. - * - * @returns Altitude in degrees. - */ -export declare function starAltitudeAt(star: JsStar, observer: JsObserver, mjd: number): number -/** - * Compute the azimuth of a star at a single instant. - * - * @returns Azimuth in degrees. - */ -export declare function starAzimuthAt(star: JsStar, observer: JsObserver, mjd: number): number -/** Find threshold-crossing events for a star. */ -export declare function starCrossings(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** Find culmination events for a star. */ -export declare function starCulminations(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number): Array -/** Find periods where a star's altitude is above a threshold. */ -export declare function starAboveThreshold(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** Find periods where a star's altitude is below a threshold. */ -export declare function starBelowThreshold(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number, thresholdDeg: number): Array -/** - * Find azimuth-crossing events for a star. - * - * @param bearingDeg — Target azimuth bearing in degrees. - * @returns Array of azimuth crossing events `{ mjd, direction }`. - */ -export declare function starAzimuthCrossings(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number, bearingDeg: number): Array -/** - * Find azimuth extrema (max/min bearing) for a star. - * - * @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`. - */ -export declare function starAzimuthExtrema(star: JsStar, observer: JsObserver, startMjd: number, endMjd: number): Array -/** - * Intersect two lists of MJD periods, returning only overlapping intervals. - * - * This is useful for combining altitude and azimuth constraints, or - * combining target visibility with astronomical night periods. - * - * @param periods1 — First list of MJD periods. - * @param periods2 — Second list of MJD periods. - * @returns Array of MJD periods representing the intersection. - * - * ```js - * const altPeriods = starAboveThreshold(star, obs, mjd0, mjd1, 25); - * const azPeriods = bodyAboveThreshold('Sun', obs, mjd0, mjd1, -18); // dark sky - * const observable = intersectPeriods(altPeriods, azPeriods); - * ``` - */ -export declare function intersectPeriods(periods1: Array, periods2: Array): Array -/** Moon phase geometry at a single instant. */ -export interface MoonPhase { - /** Phase angle in degrees (Sun-Moon-Earth/observer angle). */ - phaseAngleDeg: number - /** Fraction of the disk illuminated [0, 1]. */ - illuminatedFraction: number - /** Elongation in degrees (eastward from the Sun). */ - elongationDeg: number - /** Whether the Moon is waxing. */ - waxing: boolean - /** Named phase label (e.g. `"FullMoon"`, `"WaxingCrescent"`). */ - label: string -} -/** A principal lunar phase event (New Moon, First Quarter, Full Moon, Last Quarter). */ -export interface PhaseEvent { - /** Time of the event (Modified Julian Date). */ - mjd: number - /** Phase kind: `"NewMoon"`, `"FirstQuarter"`, `"FullMoon"`, `"LastQuarter"`. */ - kind: string -} -/** - * Compute geocentric Moon phase geometry at a Julian Date. - * - * @param jd — Julian Date. - * @returns Phase geometry including illumination, elongation, and named label. - * - * ```js - * const { moonPhase } = require('@siderust/siderust'); - * const phase = moonPhase(2451545.0); - * console.log(phase.label, phase.illuminatedFraction); - * ``` - */ -export declare function moonPhase(jd: number): MoonPhase -/** - * Compute topocentric Moon phase geometry at a Julian Date for an observer. - * - * @param jd — Julian Date. - * @param observer — Observer location on Earth. - */ -export declare function moonPhaseTopocentric(jd: number, observer: Observer): MoonPhase -/** - * Find all principal lunar phase events in a time window. - * - * @param startMjd — Window start (MJD). - * @param endMjd — Window end (MJD). - * @returns Sorted array of `{ mjd, kind }` events. - * - * ```js - * const { findPhaseEvents } = require('@siderust/siderust'); - * const events = findPhaseEvents(60000.0, 60030.0); // ~1 month - * events.forEach(e => console.log(e.kind, e.mjd)); - * ``` - */ -export declare function findPhaseEvents(startMjd: number, endMjd: number): Array -/** - * Find periods where geocentric Moon illumination is above `kMin` ∈ [0, 1]. - * - * @param startMjd — Window start (MJD). - * @param endMjd — Window end (MJD). - * @param kMin — Minimum illumination fraction. - * @returns Array of MJD periods. - */ -export declare function moonIlluminationAbove(startMjd: number, endMjd: number, kMin: number): Array -/** Find periods where geocentric Moon illumination is below `kMax` ∈ [0, 1]. */ -export declare function moonIlluminationBelow(startMjd: number, endMjd: number, kMax: number): Array -/** Find periods where geocentric Moon illumination is in `[kMin, kMax]`. */ -export declare function moonIlluminationRange(startMjd: number, endMjd: number, kMin: number, kMax: number): Array -/** List all catalog star names. */ -export declare function listCatalogStars(): Array -/** Return the siderust-node version string. */ -export declare function version(): string -export type JsObserver = Observer -/** - * A geodetic observer location on the Earth's surface (WGS84 ellipsoid). - * - * Used as the reference site for all topocentric computations: altitude, - * azimuth, crossings, culminations, and coordinate transforms to the - * Horizontal frame. - * - * ```js - * const { Observer } = require('@siderust/siderust'); - * - * // Custom site - * const obs = new Observer(-17.8925, 28.7543, 2396); - * - * // Preset observatory - * const orm = Observer.roqueDeLasMuchachos(); - * ``` - */ -export declare class Observer { - /** - * Create an observer at a geodetic position. - * - * @param lonDeg — Longitude in degrees (east positive). - * @param latDeg — Latitude in degrees (north positive). - * @param heightM — Height above WGS84 ellipsoid in metres. - */ - constructor(lonDeg: number, latDeg: number, heightM: number) - /** - * Roque de los Muchachos Observatory (La Palma, Spain). - * - * Longitude −17.8925°, Latitude +28.7543°, Altitude 2396 m. - */ - static roqueDeLasMuchachos(): JsObserver - /** - * Paranal Observatory (ESO, Chile). - * - * Longitude −70.4043°, Latitude −24.6272°, Altitude 2635 m. - */ - static elParanal(): JsObserver - /** - * Mauna Kea Observatory (Hawaiʻi, USA). - * - * Longitude −155.4681°, Latitude +19.8207°, Altitude 4207 m. - */ - static maunaKea(): JsObserver - /** - * La Silla Observatory (ESO, Chile). - * - * Longitude −70.7346°, Latitude −29.2584°, Altitude 2400 m. - */ - static laSilla(): JsObserver - /** Longitude in degrees (east positive). */ - get lonDeg(): number - /** Latitude in degrees (north positive). */ - get latDeg(): number - /** Height above the WGS84 ellipsoid in metres. */ - get heightM(): number - /** Human-readable string representation. */ - format(): string -} -/** - * A 3D cartesian position with frame, center, and unit metadata. - * - * This mirrors the Python `Position` type for API parity. - */ -export declare class Position { - /** Create a new position with explicit coordinates, frame, center, and unit. */ - constructor(x: number, y: number, z: number, frame: string, center: string, unit: string) - /** X coordinate. */ - get x(): number - /** Y coordinate. */ - get y(): number - /** Z coordinate. */ - get z(): number - /** Reference frame (e.g., "EclipticMeanJ2000", "ICRS"). */ - get frame(): string - /** Coordinate center (e.g., "Barycentric", "Heliocentric", "Geocentric"). */ - get center(): string - /** Distance unit ("au" or "km"). */ - get unit(): string - /** Euclidean distance from origin. */ - magnitude(): number - /** Subtract another position to get a displacement vector. */ - subtract(other: Position): Displacement - /** Add a displacement to get a new position. */ - addDisplacement(d: Displacement): Position -} -/** A displacement vector (difference between two positions). */ -export declare class Displacement { - /** Create a new displacement. */ - constructor(dx: number, dy: number, dz: number, frame: string, unit: string) - /** X component of displacement. */ - get dx(): number - /** Y component of displacement. */ - get dy(): number - /** Z component of displacement. */ - get dz(): number - /** Reference frame. */ - get frame(): string - /** Distance unit. */ - get unit(): string - /** Euclidean magnitude. */ - magnitude(): number - /** Scale displacement by a factor. */ - scale(factor: number): Displacement - /** Add another displacement. */ - add(other: Displacement): Displacement -} -export type JsStar = Star -/** - * A star with physical parameters and sky coordinates. - * - * Use the factory methods to look up well-known stars from the built-in - * catalog, or the constructor to define custom stars. - * - * ```js - * const { Star } = require('@siderust/siderust'); - * - * // Catalog look-up - * const vega = Star.catalog('Vega'); - * - * // Custom star - * const custom = new Star('MyStar', 100, 1.5, 1.2, 3.0, 200.0, 45.0); - * ``` - */ -export declare class Star { - /** - * Create a custom star. - * - * @param name — Display name. - * @param distanceLy — Distance in light-years. - * @param massSolar — Mass in solar masses (M☉). - * @param radiusSolar — Radius in solar radii (R☉). - * @param luminositySolar — Luminosity in solar luminosities (L☉). - * @param raDeg — Right ascension at J2000.0 in degrees. - * @param decDeg — Declination at J2000.0 in degrees. - */ - constructor(name: string, distanceLy: number, massSolar: number, radiusSolar: number, luminositySolar: number, raDeg: number, decDeg: number) - /** - * Look up a star from the built-in catalog by name. - * - * Supported names (case-insensitive): Vega, Sirius, Polaris, Canopus, - * Arcturus, Rigel, Betelgeuse, Procyon, Aldebaran, Altair. - */ - static catalog(name: string): JsStar - /** Star name. */ - get name(): string - /** Distance in light-years. */ - get distanceLy(): number - /** Mass in solar masses (M☉). */ - get massSolar(): number - /** Radius in solar radii (R☉). */ - get radiusSolar(): number - /** Luminosity in solar luminosities (L☉). */ - get luminositySolar(): number - /** Right ascension at J2000.0 in degrees. */ - get raDeg(): number - /** Declination at J2000.0 in degrees. */ - get decDeg(): number - /** Human-readable representation. */ - format(): string -} diff --git a/siderust-node/lib/Observer.js b/siderust-node/lib/Observer.js index dab17fb..e5e07d3 100644 --- a/siderust-node/lib/Observer.js +++ b/siderust-node/lib/Observer.js @@ -11,6 +11,7 @@ 'use strict'; const { Quantity } = require('@siderust/qtty'); +const backend = require('./backend.js'); class Observer { /** @@ -24,6 +25,7 @@ class Observer { this._lonDeg = _extractQuantityValue(lon, 'Degree', 'lon'); this._latDeg = _extractQuantityValue(lat, 'Degree', 'lat'); this._heightM = _extractQuantityValue(height, 'Meter', 'height'); + this._native = null; if ( !Number.isFinite(this._lonDeg) || @@ -38,38 +40,46 @@ class Observer { /** Roque de los Muchachos Observatory (La Palma, Spain). */ static roqueDeLasMuchachos() { - return new Observer( + const observer = new Observer( new Quantity(-17.8925, 'Degree'), new Quantity(28.7543, 'Degree'), new Quantity(2396, 'Meter'), ); + observer._native = backend.NativeObserver.roqueDeLasMuchachos(); + return observer; } /** Paranal Observatory (ESO, Chile). */ static elParanal() { - return new Observer( + const observer = new Observer( new Quantity(-70.4043, 'Degree'), new Quantity(-24.6272, 'Degree'), new Quantity(2635, 'Meter'), ); + observer._native = backend.NativeObserver.elParanal(); + return observer; } /** Mauna Kea Observatory (Hawaiʻi, USA). */ static maunaKea() { - return new Observer( + const observer = new Observer( new Quantity(-155.4681, 'Degree'), new Quantity(19.8207, 'Degree'), new Quantity(4207, 'Meter'), ); + observer._native = backend.NativeObserver.maunaKea(); + return observer; } /** La Silla Observatory (ESO, Chile). */ static laSilla() { - return new Observer( + const observer = new Observer( new Quantity(-70.7346, 'Degree'), new Quantity(-29.2584, 'Degree'), new Quantity(2400, 'Meter'), ); + observer._native = backend.NativeObserver.laSilla(); + return observer; } // ── typed accessors ──────────────────────────────────────────── @@ -93,6 +103,13 @@ class Observer { format() { return `Observer(lon=${this._lonDeg.toFixed(4)}°, lat=${this._latDeg.toFixed(4)}°, h=${this._heightM.toFixed(1)} m)`; } + + toNative() { + if (!this._native) { + this._native = new backend.NativeObserver(this._lonDeg, this._latDeg, this._heightM); + } + return this._native; + } } /** diff --git a/siderust-node/lib/Star.js b/siderust-node/lib/Star.js index 37c8785..b6cec87 100644 --- a/siderust-node/lib/Star.js +++ b/siderust-node/lib/Star.js @@ -33,6 +33,7 @@ class Star { this._decDeg = _extractQuantityValue(dec, 'Degree', 'dec'); this._name = name; + this._native = null; } /** @@ -43,7 +44,7 @@ class Star { static catalog(name) { // Delegate to native for the catalog data const native = backend.NativeStar.catalog(name); - return new Star( + const star = new Star( native.name, new Quantity(native.distanceLy, 'LightYear'), new Quantity(native.massSolar, 'SolarMass'), @@ -52,6 +53,8 @@ class Star { new Quantity(native.raDeg, 'Degree'), new Quantity(native.decDeg, 'Degree'), ); + star._native = native; + return star; } /** Star name. */ @@ -95,6 +98,21 @@ class Star { format() { return `Star(${this._name}, d=${this._distanceLy.toFixed(1)} ly, RA=${this._raDeg.toFixed(4)}°, Dec=${this._decDeg.toFixed(4)}°)`; } + + toNative() { + if (!this._native) { + this._native = new backend.NativeStar( + this._name, + this._distanceLy, + this._massSolar, + this._radiusSolar, + this._luminositySolar, + this._raDeg, + this._decDeg, + ); + } + return this._native; + } } /** diff --git a/siderust-node/lib/wrappers.js b/siderust-node/lib/wrappers.js index 123ec03..f791738 100644 --- a/siderust-node/lib/wrappers.js +++ b/siderust-node/lib/wrappers.js @@ -21,22 +21,14 @@ function toNativeObserver(obs) { if (!(obs instanceof Observer)) { throw new Error('Expected an Observer'); } - return new backend.NativeObserver(obs._lonDeg, obs._latDeg, obs._heightM); + return obs.toNative(); } function toNativeStar(star) { if (!(star instanceof Star)) { throw new Error('Expected a Star'); } - return new backend.NativeStar( - star._name, - star._distanceLy, - star._massSolar, - star._radiusSolar, - star._luminositySolar, - star._raDeg, - star._decDeg, - ); + return star.toNative(); } function toJdValue(jd) { diff --git a/siderust-node/siderust.linux-x64-gnu.node b/siderust-node/siderust.linux-x64-gnu.node index f11ef00..5adf5ce 100755 Binary files a/siderust-node/siderust.linux-x64-gnu.node and b/siderust-node/siderust.linux-x64-gnu.node differ diff --git a/siderust-node/src/body.rs b/siderust-node/src/body.rs index 08e305a..4b113f7 100644 --- a/siderust-node/src/body.rs +++ b/siderust-node/src/body.rs @@ -7,7 +7,6 @@ use napi_derive::napi; // Re-export shared types from the binding core. pub(crate) use siderust_binding_core::body::BodyKind; -pub(crate) use siderust_binding_core::dispatch_body; /// Convert a body name string to BodyKind, mapping errors to napi::Error. pub(crate) fn parse_body(s: &str) -> napi::Result { diff --git a/siderust-node/src/events.rs b/siderust-node/src/events.rs index ef80164..9f96bd8 100644 --- a/siderust-node/src/events.rs +++ b/siderust-node/src/events.rs @@ -1,221 +1,119 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Vallés Puig, Ramon -//! Altitude and azimuth event queries — the core observation-planning API. -//! -//! All functions accept an `Observer`, a body name (or `Star` handle), and -//! a time window, returning arrays of events or periods in a single call. +//! Altitude and azimuth event queries — thin napi wrappers over shared core logic. use napi_derive::napi; +use siderust_binding_core::events as core_events; +use tempoch::{ModifiedJulianDate, Period, MJD}; -use crate::body::{dispatch_body, parse_body}; +use crate::body::parse_body; use crate::observer::JsObserver; use crate::star::JsStar; -use qtty::*; -use siderust::calculus::altitude::{self, SearchOpts}; -use siderust::calculus::azimuth; -use siderust::AltitudePeriodsProvider; -use siderust::AzimuthProvider; -use tempoch::{ModifiedJulianDate, Period, MJD}; - -// ═══════════════════════════════════════════════════════════════════════════ -// Result types (plain objects) -// ═══════════════════════════════════════════════════════════════════════════ - /// A threshold-crossing event (rise or set). #[napi(object)] pub struct CrossingEvent { - /// Time of the crossing (Modified Julian Date). pub mjd: f64, - /// Direction: `"rising"` or `"setting"`. pub direction: String, } /// A culmination event (local altitude extremum). #[napi(object)] pub struct CulminationEvent { - /// Time of the culmination (Modified Julian Date). pub mjd: f64, - /// Altitude at the extremum in degrees. pub altitude_deg: f64, - /// Kind: `"max"` (upper culmination) or `"min"` (lower culmination). pub kind: String, } /// A time period (MJD interval). #[napi(object)] pub struct MjdPeriod { - /// Start of the period (Modified Julian Date). pub start_mjd: f64, - /// End of the period (Modified Julian Date). pub end_mjd: f64, } /// An azimuth-crossing event. #[napi(object)] pub struct AzimuthCrossingEvent { - /// Time of the event (Modified Julian Date). pub mjd: f64, - /// Crossing direction: `"rising"` or `"setting"`. pub direction: String, } /// An azimuth extremum (local max or min bearing). #[napi(object)] pub struct AzimuthExtremum { - /// Time of the extremum (Modified Julian Date). pub mjd: f64, - /// Azimuth at the extremum in degrees. pub azimuth_deg: f64, - /// Kind: `"max"` or `"min"`. pub kind: String, } -// ═══════════════════════════════════════════════════════════════════════════ -// Helpers -// ═══════════════════════════════════════════════════════════════════════════ - -pub(crate) fn make_window(start_mjd: f64, end_mjd: f64) -> napi::Result> { - if !start_mjd.is_finite() || !end_mjd.is_finite() { - return Err(napi::Error::from_reason( - "Window bounds (startMjd, endMjd) must be finite", - )); +impl From for CrossingEvent { + fn from(event: core_events::CrossingEventData) -> Self { + Self { + mjd: event.mjd, + direction: event.direction.as_str().to_string(), + } } - if start_mjd >= end_mjd { - return Err(napi::Error::from_reason( - "Window start must be before end (startMjd < endMjd)", - )); - } - Ok(Period::new( - ModifiedJulianDate::new(start_mjd), - ModifiedJulianDate::new(end_mjd), - )) } -fn convert_crossings(events: Vec) -> Vec { - events - .into_iter() - .map(|e| CrossingEvent { - mjd: e.mjd.value(), - direction: match e.direction { - altitude::CrossingDirection::Rising => "rising".to_string(), - altitude::CrossingDirection::Setting => "setting".to_string(), - }, - }) - .collect() +impl From for CulminationEvent { + fn from(event: core_events::CulminationEventData) -> Self { + Self { + mjd: event.mjd, + altitude_deg: event.altitude_deg, + kind: event.kind.as_str().to_string(), + } + } } -fn convert_culminations(events: Vec) -> Vec { - events - .into_iter() - .map(|e| CulminationEvent { - mjd: e.mjd.value(), - altitude_deg: e.altitude.value(), - kind: match e.kind { - altitude::CulminationKind::Max => "max".to_string(), - altitude::CulminationKind::Min => "min".to_string(), - }, - }) - .collect() +impl From for MjdPeriod { + fn from(period: core_events::MjdPeriodData) -> Self { + Self { + start_mjd: period.start_mjd, + end_mjd: period.end_mjd, + } + } } -fn convert_periods(periods: Vec>) -> Vec { - periods - .into_iter() - .map(|p| MjdPeriod { - start_mjd: p.start.value(), - end_mjd: p.end.value(), - }) - .collect() +impl From for AzimuthCrossingEvent { + fn from(event: core_events::AzimuthCrossingEventData) -> Self { + Self { + mjd: event.mjd, + direction: event.direction.as_str().to_string(), + } + } } -fn convert_az_crossings( - events: Vec, -) -> Vec { - events - .into_iter() - .map(|e| AzimuthCrossingEvent { - mjd: e.mjd.value(), - direction: match e.direction { - siderust::AzimuthCrossingDirection::Rising => "rising".to_string(), - siderust::AzimuthCrossingDirection::Setting => "setting".to_string(), - }, - }) - .collect() +impl From for AzimuthExtremum { + fn from(event: core_events::AzimuthExtremumData) -> Self { + Self { + mjd: event.mjd, + azimuth_deg: event.azimuth_deg, + kind: event.kind.as_str().to_string(), + } + } } -fn convert_az_extrema(events: Vec) -> Vec { - events - .into_iter() - .map(|e| AzimuthExtremum { - mjd: e.mjd.value(), - azimuth_deg: e.azimuth.value(), - kind: match e.kind { - siderust::AzimuthExtremumKind::Max => "max".to_string(), - siderust::AzimuthExtremumKind::Min => "min".to_string(), - }, - }) - .collect() +fn into_node_vec(items: Vec) -> Vec +where + U: From, +{ + items.into_iter().map(U::from).collect() } -// ═══════════════════════════════════════════════════════════════════════════ -// Body altitude — instantaneous -// ═══════════════════════════════════════════════════════════════════════════ - -/// Compute the altitude of a solar-system body at a single instant. -/// -/// @param body — Body name (e.g. `"Sun"`, `"Moon"`, `"Mars"`). -/// @param observer — Observer location. -/// @param mjd — Modified Julian Date of the instant. -/// @returns Altitude in degrees. -/// -/// ```js -/// const { Observer, bodyAltitudeAt } = require('@siderust/siderust'); -/// const obs = Observer.roqueDeLasMuchachos(); -/// const alt = bodyAltitudeAt('Sun', obs, 60000.0); -/// ``` #[napi(js_name = "bodyAltitudeAt")] pub fn body_altitude_at(body: String, observer: &JsObserver, mjd: f64) -> napi::Result { - if !mjd.is_finite() { - return Err(napi::Error::from_reason("mjd must be finite")); - } - let kind = parse_body(&body)?; - let m = ModifiedJulianDate::new(mjd); - let result: f64 = dispatch_body!(kind, |b| { - b.altitude_at(&observer.inner, m).to::().value() - }); - Ok(result) + core_events::body_altitude_at(parse_body(&body)?, &observer.inner, mjd) + .map_err(napi::Error::from_reason) } -/// Compute the azimuth of a solar-system body at a single instant. -/// -/// @returns Azimuth in degrees (north = 0°, east = 90°). #[napi(js_name = "bodyAzimuthAt")] pub fn body_azimuth_at(body: String, observer: &JsObserver, mjd: f64) -> napi::Result { - if !mjd.is_finite() { - return Err(napi::Error::from_reason("mjd must be finite")); - } - let kind = parse_body(&body)?; - let m = ModifiedJulianDate::new(mjd); - let result: f64 = dispatch_body!(kind, |b| { - b.azimuth_at(&observer.inner, m).to::().value() - }); - Ok(result) + core_events::body_azimuth_at(parse_body(&body)?, &observer.inner, mjd) + .map_err(napi::Error::from_reason) } -// ═══════════════════════════════════════════════════════════════════════════ -// Body altitude — batch events -// ═══════════════════════════════════════════════════════════════════════════ - -/// Find threshold-crossing events (rise/set) for a solar-system body. -/// -/// @param body — Body name. -/// @param observer — Observer location. -/// @param startMjd — Window start (MJD). -/// @param endMjd — Window end (MJD). -/// @param thresholdDeg — Altitude threshold in degrees (e.g. 0 for horizon). -/// @returns Array of crossing events `{ mjd, direction }`. #[napi(js_name = "bodyCrossings")] pub fn body_crossings( body: String, @@ -224,19 +122,11 @@ pub fn body_crossings( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_crossings(altitude::crossings(&b, &observer.inner, window, thr, opts)) - }); - Ok(result) + core_events::body_crossings(parse_body(&body)?, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find culmination events (altitude local extrema) for a solar-system body. -/// -/// @returns Array of culmination events `{ mjd, altitudeDeg, kind }`. #[napi(js_name = "bodyCulminations")] pub fn body_culminations( body: String, @@ -244,18 +134,11 @@ pub fn body_culminations( start_mjd: f64, end_mjd: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_culminations(altitude::culminations(&b, &observer.inner, window, opts)) - }); - Ok(result) + core_events::body_culminations(parse_body(&body)?, &observer.inner, start_mjd, end_mjd) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find periods where a body's altitude is above a threshold. -/// -/// @returns Array of MJD periods `{ startMjd, endMjd }`. #[napi(js_name = "bodyAboveThreshold")] pub fn body_above_threshold( body: String, @@ -264,25 +147,11 @@ pub fn body_above_threshold( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_periods(altitude::above_threshold( - &b, - &observer.inner, - window, - thr, - opts, - )) - }); - Ok(result) + core_events::body_above_threshold(parse_body(&body)?, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find periods where a body's altitude is below a threshold. -/// -/// @returns Array of MJD periods `{ startMjd, endMjd }`. #[napi(js_name = "bodyBelowThreshold")] pub fn body_below_threshold( body: String, @@ -291,26 +160,11 @@ pub fn body_below_threshold( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_periods(altitude::below_threshold( - &b, - &observer.inner, - window, - thr, - opts, - )) - }); - Ok(result) + core_events::body_below_threshold(parse_body(&body)?, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find azimuth-crossing events for a body. -/// -/// @param bearingDeg — Target azimuth bearing in degrees. -/// @returns Array of azimuth crossing events `{ mjd, direction }`. #[napi(js_name = "bodyAzimuthCrossings")] pub fn body_azimuth_crossings( body: String, @@ -319,25 +173,11 @@ pub fn body_azimuth_crossings( end_mjd: f64, bearing_deg: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let bearing = Degrees::new(bearing_deg); - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_az_crossings(azimuth::azimuth_crossings( - &b, - &observer.inner, - window, - bearing, - opts, - )) - }); - Ok(result) + core_events::body_azimuth_crossings(parse_body(&body)?, &observer.inner, start_mjd, end_mjd, bearing_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find azimuth extrema (max/min bearing) for a body. -/// -/// @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`. #[napi(js_name = "bodyAzimuthExtrema")] pub fn body_azimuth_extrema( body: String, @@ -345,61 +185,23 @@ pub fn body_azimuth_extrema( start_mjd: f64, end_mjd: f64, ) -> napi::Result> { - let kind = parse_body(&body)?; - let window = make_window(start_mjd, end_mjd)?; - let opts = SearchOpts::default(); - let result = dispatch_body!(kind, |b| { - convert_az_extrema(azimuth::azimuth_extrema( - &b, - &observer.inner, - window, - opts, - )) - }); - Ok(result) + core_events::body_azimuth_extrema(parse_body(&body)?, &observer.inner, start_mjd, end_mjd) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -// ═══════════════════════════════════════════════════════════════════════════ -// Star altitude — instantaneous -// ═══════════════════════════════════════════════════════════════════════════ - -/// Compute the altitude of a catalog or custom star at a single instant. -/// -/// @returns Altitude in degrees. #[napi(js_name = "starAltitudeAt")] pub fn star_altitude_at(star: &JsStar, observer: &JsObserver, mjd: f64) -> napi::Result { - if !mjd.is_finite() { - return Err(napi::Error::from_reason("mjd must be finite")); - } - let m = ModifiedJulianDate::new(mjd); - Ok(star - .inner - .altitude_at(&observer.inner, m) - .to::() - .value()) + core_events::star_altitude_at(&star.inner, &observer.inner, mjd) + .map_err(napi::Error::from_reason) } -/// Compute the azimuth of a star at a single instant. -/// -/// @returns Azimuth in degrees. #[napi(js_name = "starAzimuthAt")] pub fn star_azimuth_at(star: &JsStar, observer: &JsObserver, mjd: f64) -> napi::Result { - if !mjd.is_finite() { - return Err(napi::Error::from_reason("mjd must be finite")); - } - let m = ModifiedJulianDate::new(mjd); - Ok(star - .inner - .azimuth_at(&observer.inner, m) - .to::() - .value()) + core_events::star_azimuth_at(&star.inner, &observer.inner, mjd) + .map_err(napi::Error::from_reason) } -// ═══════════════════════════════════════════════════════════════════════════ -// Star altitude — batch events -// ═══════════════════════════════════════════════════════════════════════════ - -/// Find threshold-crossing events for a star. #[napi(js_name = "starCrossings")] pub fn star_crossings( star: &JsStar, @@ -408,19 +210,11 @@ pub fn star_crossings( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - Ok(convert_crossings(altitude::crossings( - &star.inner, - &observer.inner, - window, - thr, - opts, - ))) + core_events::star_crossings(&star.inner, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find culmination events for a star. #[napi(js_name = "starCulminations")] pub fn star_culminations( star: &JsStar, @@ -428,17 +222,11 @@ pub fn star_culminations( start_mjd: f64, end_mjd: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let opts = SearchOpts::default(); - Ok(convert_culminations(altitude::culminations( - &star.inner, - &observer.inner, - window, - opts, - ))) + core_events::star_culminations(&star.inner, &observer.inner, start_mjd, end_mjd) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find periods where a star's altitude is above a threshold. #[napi(js_name = "starAboveThreshold")] pub fn star_above_threshold( star: &JsStar, @@ -447,19 +235,11 @@ pub fn star_above_threshold( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - Ok(convert_periods(altitude::above_threshold( - &star.inner, - &observer.inner, - window, - thr, - opts, - ))) + core_events::star_above_threshold(&star.inner, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find periods where a star's altitude is below a threshold. #[napi(js_name = "starBelowThreshold")] pub fn star_below_threshold( star: &JsStar, @@ -468,26 +248,11 @@ pub fn star_below_threshold( end_mjd: f64, threshold_deg: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let thr = Degrees::new(threshold_deg); - let opts = SearchOpts::default(); - Ok(convert_periods(altitude::below_threshold( - &star.inner, - &observer.inner, - window, - thr, - opts, - ))) + core_events::star_below_threshold(&star.inner, &observer.inner, start_mjd, end_mjd, threshold_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -// ═══════════════════════════════════════════════════════════════════════════ -// Star azimuth — batch events -// ═══════════════════════════════════════════════════════════════════════════ - -/// Find azimuth-crossing events for a star. -/// -/// @param bearingDeg — Target azimuth bearing in degrees. -/// @returns Array of azimuth crossing events `{ mjd, direction }`. #[napi(js_name = "starAzimuthCrossings")] pub fn star_azimuth_crossings( star: &JsStar, @@ -496,21 +261,11 @@ pub fn star_azimuth_crossings( end_mjd: f64, bearing_deg: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let bearing = Degrees::new(bearing_deg); - let opts = SearchOpts::default(); - Ok(convert_az_crossings(azimuth::azimuth_crossings( - &star.inner, - &observer.inner, - window, - bearing, - opts, - ))) + core_events::star_azimuth_crossings(&star.inner, &observer.inner, start_mjd, end_mjd, bearing_deg) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -/// Find azimuth extrema (max/min bearing) for a star. -/// -/// @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`. #[napi(js_name = "starAzimuthExtrema")] pub fn star_azimuth_extrema( star: &JsStar, @@ -518,60 +273,37 @@ pub fn star_azimuth_extrema( start_mjd: f64, end_mjd: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; - let opts = SearchOpts::default(); - Ok(convert_az_extrema(azimuth::azimuth_extrema( - &star.inner, - &observer.inner, - window, - opts, - ))) + core_events::star_azimuth_extrema(&star.inner, &observer.inner, start_mjd, end_mjd) + .map(into_node_vec) + .map_err(napi::Error::from_reason) } -// ═══════════════════════════════════════════════════════════════════════════ -// Period utilities -// ═══════════════════════════════════════════════════════════════════════════ - -/// Intersect two lists of MJD periods, returning only overlapping intervals. -/// -/// This is useful for combining altitude and azimuth constraints, or -/// combining target visibility with astronomical night periods. -/// -/// @param periods1 — First list of MJD periods. -/// @param periods2 — Second list of MJD periods. -/// @returns Array of MJD periods representing the intersection. -/// -/// ```js -/// const altPeriods = starAboveThreshold(star, obs, mjd0, mjd1, 25); -/// const azPeriods = bodyAboveThreshold('Sun', obs, mjd0, mjd1, -18); // dark sky -/// const observable = intersectPeriods(altPeriods, azPeriods); -/// ``` #[napi(js_name = "intersectPeriods")] -pub fn intersect_periods_js( - periods1: Vec, - periods2: Vec, -) -> Vec { +pub fn intersect_periods_js(periods1: Vec, periods2: Vec) -> Vec { let p1: Vec> = periods1 .iter() - .map(|p| Period::new( - ModifiedJulianDate::new(p.start_mjd), - ModifiedJulianDate::new(p.end_mjd), - )) + .map(|period| { + Period::new( + ModifiedJulianDate::new(period.start_mjd), + ModifiedJulianDate::new(period.end_mjd), + ) + }) .collect(); let p2: Vec> = periods2 .iter() - .map(|p| Period::new( - ModifiedJulianDate::new(p.start_mjd), - ModifiedJulianDate::new(p.end_mjd), - )) + .map(|period| { + Period::new( + ModifiedJulianDate::new(period.start_mjd), + ModifiedJulianDate::new(period.end_mjd), + ) + }) .collect(); - let result = tempoch::intersect_periods(&p1, &p2); - result + tempoch::intersect_periods(&p1, &p2) .into_iter() - .map(|p| MjdPeriod { - start_mjd: p.start.value(), - end_mjd: p.end.value(), + .map(|period| MjdPeriod { + start_mjd: period.start.value(), + end_mjd: period.end.value(), }) .collect() } diff --git a/siderust-node/src/observer.rs b/siderust-node/src/observer.rs index 0440436..13e6297 100644 --- a/siderust-node/src/observer.rs +++ b/siderust-node/src/observer.rs @@ -4,11 +4,9 @@ //! Observer (geodetic site) for topocentric computations. use napi_derive::napi; - -use qtty::*; use siderust::coordinates::centers::Geodetic; use siderust::coordinates::frames::ECEF; -use siderust::observatories; +use siderust_binding_core::observer as core_observer; // ───────────────────────────────────────────────────────────────────────────── // Observer class @@ -43,17 +41,10 @@ impl JsObserver { /// @param heightM — Height above WGS84 ellipsoid in metres. #[napi(constructor)] pub fn new(lon_deg: f64, lat_deg: f64, height_m: f64) -> napi::Result { - if !lon_deg.is_finite() || !lat_deg.is_finite() || !height_m.is_finite() { - return Err(napi::Error::from_reason( - "Observer coordinates must be finite (not NaN or ±infinity)", - )); - } + core_observer::validate_observer_coords(lon_deg, lat_deg, height_m) + .map_err(napi::Error::from_reason)?; Ok(Self { - inner: Geodetic::::new( - Degrees::new(lon_deg), - Degrees::new(lat_deg), - Meters::new(height_m), - ), + inner: core_observer::create_observer(lon_deg, lat_deg, height_m), }) } @@ -65,7 +56,7 @@ impl JsObserver { #[napi(factory, js_name = "roqueDeLasMuchachos")] pub fn roque_de_las_muchachos() -> Self { Self { - inner: observatories::ROQUE_DE_LOS_MUCHACHOS, + inner: core_observer::roque_de_los_muchachos(), } } @@ -75,7 +66,7 @@ impl JsObserver { #[napi(factory, js_name = "elParanal")] pub fn el_paranal() -> Self { Self { - inner: observatories::EL_PARANAL, + inner: core_observer::el_paranal(), } } @@ -85,7 +76,7 @@ impl JsObserver { #[napi(factory, js_name = "maunaKea")] pub fn mauna_kea() -> Self { Self { - inner: observatories::MAUNA_KEA, + inner: core_observer::mauna_kea(), } } @@ -95,7 +86,7 @@ impl JsObserver { #[napi(factory, js_name = "laSilla")] pub fn la_silla() -> Self { Self { - inner: observatories::LA_SILLA_OBSERVATORY, + inner: core_observer::la_silla(), } } @@ -104,19 +95,19 @@ impl JsObserver { /// Longitude in degrees (east positive). #[napi(getter, js_name = "lonDeg")] pub fn lon_deg(&self) -> f64 { - self.inner.lon.value() + core_observer::get_observer_lon_deg(&self.inner) } /// Latitude in degrees (north positive). #[napi(getter, js_name = "latDeg")] pub fn lat_deg(&self) -> f64 { - self.inner.lat.value() + core_observer::get_observer_lat_deg(&self.inner) } /// Height above the WGS84 ellipsoid in metres. #[napi(getter, js_name = "heightM")] pub fn height_m(&self) -> f64 { - self.inner.height.value() + core_observer::get_observer_height_m(&self.inner) } /// Human-readable string representation. @@ -124,9 +115,9 @@ impl JsObserver { pub fn format(&self) -> String { format!( "Observer(lon={:.4}°, lat={:.4}°, h={:.1} m)", - self.inner.lon.value(), - self.inner.lat.value(), - self.inner.height.value(), + self.lon_deg(), + self.lat_deg(), + self.height_m(), ) } } diff --git a/siderust-node/src/phase.rs b/siderust-node/src/phase.rs index d1c37a0..62c7a6c 100644 --- a/siderust-node/src/phase.rs +++ b/siderust-node/src/phase.rs @@ -4,8 +4,9 @@ //! Lunar phase queries — geometry, events, and illumination periods. use napi_derive::napi; +use siderust_binding_core::events::make_window; -use crate::events::{make_window, MjdPeriod}; +use crate::events::MjdPeriod; use crate::observer::JsObserver; use qtty::*; @@ -112,7 +113,7 @@ pub fn moon_phase_topo(jd: f64, observer: &JsObserver) -> napi::Result napi::Result> { - let window = make_window(start_mjd, end_mjd)?; + let window = make_window(start_mjd, end_mjd).map_err(napi::Error::from_reason)?; let events = find_phase_events::(window, PhaseSearchOpts::default()); Ok(events .into_iter() @@ -139,7 +140,7 @@ pub fn moon_illumination_above( end_mjd: f64, k_min: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; + let window = make_window(start_mjd, end_mjd).map_err(napi::Error::from_reason)?; let periods = illumination_above::(window, k_min, PhaseSearchOpts::default()); Ok(periods .into_iter() @@ -157,7 +158,7 @@ pub fn moon_illumination_below( end_mjd: f64, k_max: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; + let window = make_window(start_mjd, end_mjd).map_err(napi::Error::from_reason)?; let periods = illumination_below::(window, k_max, PhaseSearchOpts::default()); Ok(periods .into_iter() @@ -176,7 +177,7 @@ pub fn moon_illumination_range( k_min: f64, k_max: f64, ) -> napi::Result> { - let window = make_window(start_mjd, end_mjd)?; + let window = make_window(start_mjd, end_mjd).map_err(napi::Error::from_reason)?; let periods = illumination_range::(window, k_min, k_max, PhaseSearchOpts::default()); Ok(periods diff --git a/siderust-node/src/star.rs b/siderust-node/src/star.rs index 2fe8949..6713567 100644 --- a/siderust-node/src/star.rs +++ b/siderust-node/src/star.rs @@ -4,16 +4,8 @@ //! Star — catalog and custom stars for altitude/azimuth/coordinate queries. use napi_derive::napi; - -use qtty::length::nominal::SolarRadiuses; -use qtty::*; -use siderust::bodies::{self, Star}; -use siderust::coordinates::centers::Geocentric; -use siderust::coordinates::frames::EquatorialMeanJ2000; -use siderust::coordinates::spherical; -use siderust::targets::CoordinateWithPM; -use siderust::time::JulianDate; -use std::borrow::Cow; +use siderust::bodies::Star; +use siderust_binding_core::star as core_star; // ───────────────────────────────────────────────────────────────────────────── // Star class @@ -59,42 +51,17 @@ impl JsStar { ra_deg: f64, dec_deg: f64, ) -> napi::Result { - for (label, v) in [ - ("distanceLy", distance_ly), - ("massSolar", mass_solar), - ("radiusSolar", radius_solar), - ("luminositySolar", luminosity_solar), - ("raDeg", ra_deg), - ("decDeg", dec_deg), - ] { - if !v.is_finite() { - return Err(napi::Error::from_reason(format!( - "{label} must be finite (not NaN or ±infinity)" - ))); - } - } - - let pos = spherical::Position::::new( - Degrees::new(ra_deg), - Degrees::new(dec_deg), - LightYears::new(distance_ly), - ); - let target = - CoordinateWithPM::>::new_static( - pos, - JulianDate::J2000, - ); - - let star = Star::new( - Cow::<'static, str>::Owned(name), - LightYears::new(distance_ly), - SolarMasses::new(mass_solar), - SolarRadiuses::new(radius_solar), - SolarLuminosities::new(luminosity_solar), - target, - ); - - Ok(Self { inner: star }) + core_star::create_star( + name, + distance_ly, + mass_solar, + radius_solar, + luminosity_solar, + ra_deg, + dec_deg, + ) + .map(|inner| Self { inner }) + .map_err(napi::Error::from_reason) } /// Look up a star from the built-in catalog by name. @@ -103,26 +70,9 @@ impl JsStar { /// Arcturus, Rigel, Betelgeuse, Procyon, Aldebaran, Altair. #[napi(factory)] pub fn catalog(name: String) -> napi::Result { - let star: &Star<'static> = match name.to_uppercase().as_str() { - "VEGA" => &bodies::VEGA, - "SIRIUS" => &bodies::SIRIUS, - "POLARIS" => &bodies::POLARIS, - "CANOPUS" => &bodies::CANOPUS, - "ARCTURUS" => &bodies::ARCTURUS, - "RIGEL" => &bodies::RIGEL, - "BETELGEUSE" => &bodies::BETELGEUSE, - "PROCYON" => &bodies::PROCYON, - "ALDEBARAN" => &bodies::ALDEBARAN, - "ALTAIR" => &bodies::ALTAIR, - _ => { - return Err(napi::Error::from_reason(format!( - "Unknown catalog star: \"{name}\". Available: Vega, Sirius, Polaris, Canopus, Arcturus, Rigel, Betelgeuse, Procyon, Aldebaran, Altair." - ))); - } - }; - Ok(Self { - inner: star.clone(), - }) + core_star::catalog_star(&name) + .map(|inner| Self { inner }) + .map_err(napi::Error::from_reason) } // ── accessors ──────────────────────────────────────────────────── @@ -185,16 +135,5 @@ impl JsStar { /// List all catalog star names. #[napi(js_name = "listCatalogStars")] pub fn list_catalog_stars() -> Vec { - vec![ - "Vega".into(), - "Sirius".into(), - "Polaris".into(), - "Canopus".into(), - "Arcturus".into(), - "Rigel".into(), - "Betelgeuse".into(), - "Procyon".into(), - "Aldebaran".into(), - "Altair".into(), - ] + core_star::list_catalog_star_names() } diff --git a/siderust-web/Cargo.lock b/siderust-web/Cargo.lock index fd276f5..3111531 100644 --- a/siderust-web/Cargo.lock +++ b/siderust-web/Cargo.lock @@ -4,9 +4,7 @@ version = 4 [[package]] name = "affn" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6639fb5f399ec93eceeec24afc3608b9760ddcdece6936412d04ab478627d2" +version = "0.5.0" dependencies = [ "affn-derive", "nalgebra", @@ -16,8 +14,6 @@ dependencies = [ [[package]] name = "affn-derive" version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20fe1fdcdc9c8b7fa557c99dc1acacbc90d7c64c8d2826e83f01e414fca1cf18" dependencies = [ "proc-macro2", "quote", @@ -424,8 +420,6 @@ dependencies = [ [[package]] name = "qtty" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dc60ed0161fc68ec0b0faf090e4feec70dd179447c6ca14e5cc052b4eb2a789" dependencies = [ "qtty-core 0.4.1", "qtty-derive 0.4.1", @@ -445,8 +439,6 @@ dependencies = [ [[package]] name = "qtty-core" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab488371c17ea2db038caa2a3d0044a85a433c4c2172467978d05ee39d22b6fb" dependencies = [ "libm", "qtty-derive 0.4.1", @@ -467,8 +459,6 @@ dependencies = [ [[package]] name = "qtty-derive" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab6c550316cbdd43e4eca2c94c14c1cf4e67b253278a94ccc4a5eca2ce5db6e" dependencies = [ "proc-macro2", "quote", @@ -602,8 +592,6 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siderust" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9322877f7730d54aef3db9f91f73b918c82d7ffe950ef3272a7f6d40b8e469dc" dependencies = [ "affn", "anyhow", @@ -616,7 +604,7 @@ dependencies = [ "regex", "tempoch", "walkdir", - "wide 1.1.1", + "wide 1.2.0", ] [[package]] @@ -674,8 +662,6 @@ dependencies = [ [[package]] name = "tempoch" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acec780480096b36a1872a5131de3e90463083d0a5c5bd13779defc18a530384" dependencies = [ "tempoch-core", ] @@ -683,8 +669,6 @@ dependencies = [ [[package]] name = "tempoch-core" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3f7d4b6138b4144837ed3d38042737f16ce3d909661f193fdaf3f40faf7422" dependencies = [ "chrono", "qtty 0.4.1", @@ -769,9 +753,9 @@ dependencies = [ [[package]] name = "wide" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac11b009ebeae802ed758530b6496784ebfee7a87b9abfbcaf3bbe25b814eb25" +checksum = "198f6abc41fab83526d10880fa5c17e2b4ee44e763949b4bb34e2fd1e8ca48e4" dependencies = [ "bytemuck", "safe_arch 1.0.0", diff --git a/siderust-web/Cargo.toml b/siderust-web/Cargo.toml index f1aaa7a..7756d75 100644 --- a/siderust-web/Cargo.toml +++ b/siderust-web/Cargo.toml @@ -16,7 +16,7 @@ siderust = { version = "0.6.0", features = [] } siderust-binding-core = { path = "../siderust-core" } qtty = "0.4.1" tempoch = "0.4" -affn = { version = "0.4.1", features = ["astro"] } +affn = { version = "0.5.0", features = ["astro"] } chrono = "0.4.44" wasm-bindgen = "0.2" @@ -32,3 +32,9 @@ wasm-opt = false lto = true strip = true opt-level = "s" + +[patch.crates-io] +affn = { path = "../../../rust/affn" } +qtty = { path = "../../../rust/qtty/qtty" } +siderust = { path = "../../../rust/siderust" } +tempoch = { path = "../../../rust/tempoch/tempoch" } diff --git a/siderust-web/__test__/index.test.mjs b/siderust-web/__test__/index.test.mjs index 2c87f33..589e393 100644 --- a/siderust-web/__test__/index.test.mjs +++ b/siderust-web/__test__/index.test.mjs @@ -22,11 +22,11 @@ import { } from "../index.js"; const qttyWasm = new URL( - "../../../qtty-js/qtty-web/pkg/qtty_web_bg.wasm", + "../../qtty-js/qtty-web/pkg/qtty_web_bg.wasm", import.meta.url, ); const tempochWasm = new URL( - "../../../tempoch-js/tempoch-web/pkg/tempoch_web_bg.wasm", + "../../tempoch-js/tempoch-web/pkg/tempoch_web_bg.wasm", import.meta.url, ); const siderustWasm = new URL("../pkg/siderust_web_bg.wasm", import.meta.url); diff --git a/siderust-web/examples/github-pages/README.md b/siderust-web/examples/github-pages/README.md index 7c770d5..c92718a 100644 --- a/siderust-web/examples/github-pages/README.md +++ b/siderust-web/examples/github-pages/README.md @@ -13,7 +13,7 @@ npm run build cd ../../tempoch-js/tempoch-web npm run build -cd ../../siderust-js/siderust-web +cd ../../siderust-web npm run build ``` diff --git a/siderust-web/examples/github-pages/index.html b/siderust-web/examples/github-pages/index.html index 3277274..acb45c6 100644 --- a/siderust-web/examples/github-pages/index.html +++ b/siderust-web/examples/github-pages/index.html @@ -57,8 +57,8 @@