From 9603c7c70b8d75d119b921f6ee454979a91f0d15 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 21:48:39 +0100 Subject: [PATCH 1/6] Add azimuth and lunar phase computations, enhance target handling - Introduced azimuth computations for Sun, Moon, stars, and ICRS directions in `azimuth.hpp`. - Added lunar phase geometry and events handling in `lunar_phase.hpp`. - Implemented a `Target` class for fixed ICRS pointing with altitude and azimuth computations. - Updated `siderust.hpp` to include new headers for azimuth and lunar phase functionalities. - Modified `spherical.hpp` to correct azimuth and polar coordinate handling. - Enhanced time handling in `time.hpp` to support quantity-based arithmetic. - Updated tests to reflect changes in period handling and quantity usage. --- .clang-format | 19 +- CMakeLists.txt | 31 +- docs/mainpage.md | 9 +- examples/altitude_events_example.cpp | 143 ++------- examples/coordinate_systems_example.cpp | 71 +---- examples/coordinates_examples.cpp | 102 ++---- examples/demo.cpp | 112 +------ examples/solar_system_bodies_example.cpp | 69 +--- include/siderust/azimuth.hpp | 353 +++++++++++++++++++++ include/siderust/coordinates/spherical.hpp | 8 +- include/siderust/lunar_phase.hpp | 265 ++++++++++++++++ include/siderust/siderust.hpp | 3 + include/siderust/target.hpp | 252 +++++++++++++++ include/siderust/time.hpp | 9 +- qtty-cpp | 2 +- siderust | 2 +- tempoch-cpp | 2 +- tests/test_altitude.cpp | 10 +- tests/test_time.cpp | 72 ++++- 19 files changed, 1063 insertions(+), 471 deletions(-) create mode 100644 include/siderust/azimuth.hpp create mode 100644 include/siderust/lunar_phase.hpp create mode 100644 include/siderust/target.hpp diff --git a/.clang-format b/.clang-format index 8b81670..8fe01d1 100644 --- a/.clang-format +++ b/.clang-format @@ -1,10 +1,13 @@ --- +Language: Cpp BasedOnStyle: LLVM -IndentWidth: 4 -ContinuationIndentWidth: 4 -ColumnLimit: 0 -PointerAlignment: Left -ReferenceAlignment: Left -AlignConsecutiveAssignments: Consecutive -AlignConsecutiveDeclarations: Consecutive -... +IndentWidth: 2 +ColumnLimit: 80 +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: All +AllowShortEnumsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AlwaysBreakTemplateDeclarations: MultiLine + diff --git a/CMakeLists.txt b/CMakeLists.txt index 751413a..55eb949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ endif() # --------------------------------------------------------------------------- # Platform-specific library paths # --------------------------------------------------------------------------- -set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/target/release) +set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/siderust-ffi/target/release) if(APPLE) set(SIDERUST_LIBRARY_PATH ${SIDERUST_ARTIFACT_DIR}/libsiderust_ffi.dylib) @@ -47,11 +47,14 @@ endif() # Build siderust-ffi (depends on tempoch-ffi via tempoch-cpp subdirectory) add_custom_target( build_siderust_ffi - COMMAND ${CARGO_BIN} build --release ${_SIDERUST_FEATURES_ARGS} + # Use `cargo rustc --crate-type cdylib` so that the shared library is + # produced even though Cargo.toml only lists rlib (which keeps coverage + # instrumentation clean during `cargo test`/`cargo llvm-cov`). + COMMAND ${CARGO_BIN} rustc --release --crate-type cdylib ${_SIDERUST_FEATURES_ARGS} WORKING_DIRECTORY ${SIDERUST_FFI_DIR} BYPRODUCTS ${SIDERUST_LIBRARY_PATH} DEPENDS build_tempoch_ffi - COMMENT "Building siderust-ffi via Cargo" + COMMENT "Building siderust-ffi via Cargo (cdylib override)" VERBATIM ) @@ -101,13 +104,17 @@ if(SIDERUST_BUILD_DOCS) set(SIDERUST_DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile.siderust_cpp) configure_file(${SIDERUST_DOXYFILE_IN} ${SIDERUST_DOXYFILE_OUT} @ONLY) - add_custom_target(docs - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen - COMMAND ${DOXYGEN_EXECUTABLE} ${SIDERUST_DOXYFILE_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM - ) + if(NOT TARGET docs) + add_custom_target(docs + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen + COMMAND ${DOXYGEN_EXECUTABLE} ${SIDERUST_DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM + ) + else() + message(STATUS "Top-level 'docs' target already exists; skipping creation to avoid conflict with subprojects") + endif() else() message(STATUS "Doxygen not found; 'docs' target will not be available") endif() @@ -115,9 +122,9 @@ endif() # RPATH for shared library lookup at runtime if(APPLE) - set(_siderust_rpath "@loader_path/../siderust/target/release;@loader_path/../tempoch-cpp/tempoch/tempoch-ffi/target/release;@loader_path/../qtty-cpp/qtty/target/release") + set(_siderust_rpath "@loader_path/../siderust/siderust-ffi/target/release;@loader_path/../tempoch-cpp/tempoch/tempoch-ffi/target/release;@loader_path/../qtty-cpp/qtty/target/release") elseif(UNIX) - set(_siderust_rpath "$ORIGIN/../siderust/target/release:$ORIGIN/../tempoch-cpp/tempoch/tempoch-ffi/target/release:$ORIGIN/../qtty-cpp/qtty/target/release") + set(_siderust_rpath "$ORIGIN/../siderust/siderust-ffi/target/release:$ORIGIN/../tempoch-cpp/tempoch/tempoch-ffi/target/release:$ORIGIN/../qtty-cpp/qtty/target/release") endif() # --------------------------------------------------------------------------- diff --git a/docs/mainpage.md b/docs/mainpage.md index 563c5ff..f378652 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -29,7 +29,8 @@ codebase without writing a single line of Rust. ```cpp #include -#include +#include +#include int main() { using namespace siderust; @@ -41,17 +42,17 @@ int main() { // Sun altitude at the observatory qtty::Radian alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad\n", alt.value()); + std::cout << std::fixed << std::setprecision(4) << "Sun altitude: " << alt << " rad\n"; // Star from built-in catalog const auto& vega = VEGA; qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad\n", star_alt.value()); + std::cout << "Vega altitude: " << star_alt << " rad\n"; // Astronomical night periods (twilight < -18°) auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); for (auto& p : nights) - std::printf("Night: MJD %.4f – %.4f\n", p.start_mjd(), p.end_mjd()); + std::cout << "Night: MJD " << p.start() << " – " << p.end() << "\n"; return 0; } diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index a3b1a92..30d6adb 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -1,140 +1,41 @@ /** * @file altitude_events_example.cpp * @example altitude_events_example.cpp - * @brief Altitude windows, crossings, and culminations for Sun, Moon, and - * stars. - * - * Usage: - * cmake --build build-make --target altitude_events_example - * ./build-make/altitude_events_example + * @brief Concise altitude events example using streamed UTC and Period printing. */ #include - -#include +#include +#include #include -using namespace siderust; -using namespace qtty::literals; - -static const char* crossing_direction_name(CrossingDirection d) { - return (d == CrossingDirection::Rising) ? "rising" : "setting"; -} - -static const char* culmination_kind_name(CulminationKind k) { - return (k == CulminationKind::Max) ? "max" : "min"; -} +int main() { + using namespace siderust; + using namespace qtty::literals; -static void print_utc(const UTC& utc) { - std::printf("%04d-%02u-%02u %02u:%02u:%02u", utc.year, utc.month, utc.day, - utc.hour, utc.minute, utc.second); -} + std::cout << "=== altitude_events_example (concise) ===\n"; -static void print_periods(const char* title, const std::vector& periods, - std::size_t max_items = 4) { - std::printf("%s: %zu period(s)\n", title, periods.size()); - const std::size_t n = - (periods.size() < max_items) ? periods.size() : max_items; - for (std::size_t i = 0; i < n; ++i) { - const auto s = periods[i].start().to_utc(); - const auto e = periods[i].end().to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(s); - std::printf(" -> "); - print_utc(e); - std::printf(" (%.2f h)\n", periods[i].duration_days() * 24.0); - } - if (periods.size() > n) { - std::printf(" ... (%zu more)\n", periods.size() - n); - } -} + const auto obs = MAUNA_KEA; + const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const auto end = start + qtty::Day(2.0); -static void print_crossings(const char* title, - const std::vector& events, - std::size_t max_items = 6) { - std::printf("%s: %zu event(s)\n", title, events.size()); - const std::size_t n = (events.size() < max_items) ? events.size() : max_items; - for (std::size_t i = 0; i < n; ++i) { - const auto t = events[i].time.to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(t); - std::printf(" %s\n", crossing_direction_name(events[i].direction)); - } - if (events.size() > n) { - std::printf(" ... (%zu more)\n", events.size() - n); - } -} + auto nights = sun::below_threshold(obs, start, end, -18.0_deg); + std::cout << "Astronomical nights found: " << nights.size() << "\n"; -static void print_culminations(const char* title, - const std::vector& events, - std::size_t max_items = 6) { - std::printf("%s: %zu event(s)\n", title, events.size()); - const std::size_t n = (events.size() < max_items) ? events.size() : max_items; + // Print up to three night periods as UTC ranges and duration in hours + const std::size_t n = std::min(nights.size(), 3); for (std::size_t i = 0; i < n; ++i) { - const auto t = events[i].time.to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(t); - std::printf(" alt=%.3f deg kind=%s\n", events[i].altitude.value(), - culmination_kind_name(events[i].kind)); + const auto &p = nights[i]; + std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " << p.end().to_utc() + << " (" << std::fixed << std::setprecision(2) << p.duration().value() << " h)\n"; } - if (events.size() > n) { - std::printf(" ... (%zu more)\n", events.size() - n); - } -} - -int main() { - std::printf("=== Altitude Events Example ===\n\n"); - - const auto obs = MAUNA_KEA; - const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const auto end = start + 2.0; - const Period window(start, end); - - std::printf("Observer: Mauna Kea (lon=%.4f lat=%.4f h=%.0f m)\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - std::printf("Window MJD: %.6f -> %.6f\n\n", start.value(), end.value()); - - SearchOptions opts; - opts.with_scan_step(1.0 / 144.0).with_tolerance(1e-10); - - // Sun examples. - const auto sun_night = sun::below_threshold(obs, window, -18.0_deg, opts); - const auto sun_cross = sun::crossings(obs, window, -0.833_deg, opts); - const auto sun_culm = sun::culminations(obs, window, opts); - print_periods("Sun below -18 deg (astronomical night)", sun_night); - print_crossings("Sun crossings at -0.833 deg", sun_cross); - print_culminations("Sun culminations", sun_culm); - std::printf("\n"); - - // Moon examples. - const auto moon_above = moon::above_threshold(obs, window, 20.0_deg, opts); - const auto moon_cross = moon::crossings(obs, window, 0.0_deg, opts); - const auto moon_culm = moon::culminations(obs, window, opts); - print_periods("Moon above +20 deg", moon_above); - print_crossings("Moon horizon crossings", moon_cross); - print_culminations("Moon culminations", moon_culm); - std::printf("\n"); - // Star examples. - const auto& vega = VEGA; - const auto vega_above = - star_altitude::above_threshold(vega, obs, window, 25.0_deg, opts); - const auto vega_cross = - star_altitude::crossings(vega, obs, window, 0.0_deg, opts); - const auto vega_culm = star_altitude::culminations(vega, obs, window, opts); - print_periods("VEGA above +25 deg", vega_above); - print_crossings("VEGA horizon crossings", vega_cross); - print_culminations("VEGA culminations", vega_culm); - std::printf("\n"); + auto crossings = sun::crossings(obs, start, end, 0.0_deg); + std::cout << "Sun crossings: " << crossings.size() << "\n"; - // Fixed ICRS direction examples. - const spherical::direction::ICRS dir_icrs(279.23473_deg, 38.78369_deg); - const auto dir_above = - icrs_altitude::above_threshold(dir_icrs, obs, window, 30.0_deg, opts); - const auto dir_below = - icrs_altitude::below_threshold(dir_icrs, obs, window, 0.0_deg, opts); - print_periods("Fixed ICRS direction above +30 deg", dir_above); - print_periods("Fixed ICRS direction below horizon", dir_below); + auto culminations = sun::culminations(obs, start, end); + std::cout << "Culminations: " << culminations.size() << "\n"; + std::cout << "Done.\n"; return 0; } diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index b7eeb42..5cf8f9c 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -10,74 +10,19 @@ #include -#include - -using namespace siderust; -using namespace siderust::frames; -using namespace qtty::literals; +#include +#include int main() { - std::printf("=== Coordinate Systems Example ===\n\n"); - - auto obs = ROQUE_DE_LOS_MUCHACHOS; - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - std::printf("Observer (Geodetic): lon=%.4f deg lat=%.4f deg h=%.1f m\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - - auto ecef_m = obs.to_cartesian(); - auto ecef_km = obs.to_cartesian(); - std::printf("Observer (ECEF): x=%.2f m y=%.2f m z=%.2f m\n", - ecef_m.x().value(), ecef_m.y().value(), ecef_m.z().value()); - std::printf("Observer (ECEF): x=%.2f km y=%.2f km z=%.2f km\n\n", - ecef_km.x().value(), ecef_km.y().value(), ecef_km.z().value()); + using namespace siderust; + using namespace qtty::literals; - // Vega J2000 ICRS direction. - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + std::cout << "=== coordinate_systems_example===\n"; - auto vega_ecl = vega_icrs.to(jd); - auto vega_eq_mod = vega_icrs.to(jd); - auto vega_eq_tod = vega_icrs.to(jd); - auto vega_hor = vega_icrs.to_horizontal(jd, obs); - auto vega_back = vega_ecl.to(jd); - - std::printf("Vega ICRS: RA=%.6f Dec=%.6f\n", - vega_icrs.ra().value(), vega_icrs.dec().value()); - std::printf("Vega EclipticMeanJ2000: lon=%.6f lat=%.6f\n", - vega_ecl.lon().value(), vega_ecl.lat().value()); - std::printf("Vega EquatorialMeanOfDate: RA=%.6f Dec=%.6f\n", - vega_eq_mod.ra().value(), vega_eq_mod.dec().value()); - std::printf("Vega EquatorialTrueOfDate: RA=%.6f Dec=%.6f\n", - vega_eq_tod.ra().value(), vega_eq_tod.dec().value()); - std::printf("Vega Horizontal: az=%.6f alt=%.6f\n", - vega_hor.az().value(), vega_hor.alt().value()); - std::printf("Vega roundtrip ICRS<-Ecliptic: RA=%.6f Dec=%.6f\n\n", - vega_back.ra().value(), vega_back.dec().value()); - - spherical::position::ICRS target_sph_au( - 120.0_deg, -25.0_deg, 2.0_au); - auto target_dir = target_sph_au.direction(); - std::printf("Spherical ICRS position: RA=%.2f Dec=%.2f dist=%.3f AU\n", - target_sph_au.ra().value(), - target_sph_au.dec().value(), - target_sph_au.distance().value()); - std::printf("Direction extracted from spherical position: RA=%.2f Dec=%.2f\n\n", - target_dir.ra().value(), target_dir.dec().value()); - - cartesian::position::ICRS target_cart_m(1.5e11, -3.0e10, 2.0e10); - cartesian::position::ICRS target_cart_au( - target_cart_m.x().to(), - target_cart_m.y().to(), - target_cart_m.z().to()); + auto obs = ROQUE_DE_LOS_MUCHACHOS; - std::printf("Cartesian ICRS position: x=%.3e m y=%.3e m z=%.3e m\n", - target_cart_m.x().value(), - target_cart_m.y().value(), - target_cart_m.z().value()); - std::printf("Cartesian ICRS position: x=%.6f AU y=%.6f AU z=%.6f AU\n", - target_cart_au.x().value(), - target_cart_au.y().value(), - target_cart_au.z().value()); + std::cout << "Observer lon=" << std::fixed << std::setprecision(4) << obs.lon + << " lat=" << obs.lat << "\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index f32fa07..0a2ecf5 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -2,100 +2,34 @@ * @file coordinates_examples.cpp * @example coordinates_examples.cpp * @brief Focused examples for creating and converting typed coordinates. - * - * Usage: - * cmake --build build-make --target coordinates_examples - * ./build-make/coordinates_examples */ - #include -#include +#include +#include -using namespace siderust; -using namespace qtty::literals; +int main() { + using namespace siderust; + using namespace qtty::literals; -static void geodetic_and_ecef_example() { - std::printf("1) Geodetic -> ECEF cartesian\n"); + std::cout << "=== coordinates_examples (concise) ===\n"; + // Geodetic -> ECEF (single line) Geodetic obs(-17.8890, 28.7610, 2396.0); - auto ecef = obs.to_cartesian(); - auto ecef_km = obs.to_cartesian(); - - std::printf(" Geodetic lon=%.4f deg lat=%.4f deg h=%.1f m\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - std::printf(" ECEF x=%.2f m y=%.2f m z=%.2f m\n\n", - ecef.x().value(), ecef.y().value(), ecef.z().value()); - std::printf(" ECEF x=%.2f km y=%.2f km z=%.2f km\n\n", - ecef_km.x().value(), ecef_km.y().value(), ecef_km.z().value()); -} - -static void spherical_direction_example() { - std::printf("2) Spherical direction frame conversions\n"); + auto ecef = obs.to_cartesian(); + std::cout << "Geodetic lon=" << std::fixed << std::setprecision(4) << obs.lon.value() + << " lat=" << obs.lat.value() << " h=" << obs.height.value() << " m\n"; + // Spherical direction example (ICRS -> horizontal) spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - auto ecl = vega_icrs.to(jd); - auto eq_mod = vega_icrs.to(jd); - auto hor = vega_icrs.to_horizontal(jd, ROQUE_DE_LOS_MUCHACHOS); - - std::printf(" ICRS RA=%.5f Dec=%.5f\n", vega_icrs.ra().value(), vega_icrs.dec().value()); - std::printf(" Ecliptic lon=%.5f lat=%.5f\n", ecl.lon().value(), ecl.lat().value()); - std::printf(" Equatorial(MOD) RA=%.5f Dec=%.5f\n", eq_mod.ra().value(), eq_mod.dec().value()); - std::printf(" Horizontal az=%.5f alt=%.5f\n\n", hor.az().value(), hor.alt().value()); -} - -static void spherical_position_example() { - std::printf("3) Spherical position + extracting direction\n"); - - spherical::position::ICRS target( - 120.0_deg, -25.0_deg, 2.0e17_m); - auto dir = target.direction(); - - std::printf(" Position RA=%.2f Dec=%.2f dist=%.3e m\n", - target.ra().value(), target.dec().value(), target.distance().value()); - std::printf(" Direction-only RA=%.2f Dec=%.2f\n\n", - dir.ra().value(), dir.dec().value()); -} - -static void cartesian_and_units_example() { - std::printf("4) Cartesian coordinate creation + unit conversion\n"); - - cartesian::Direction axis_x(1.0, 0.0, 0.0); - cartesian::position::EclipticMeanJ2000 sample_helio_au(1.0, 0.25, -0.1); - - auto x_km = sample_helio_au.x().to(); - auto y_km = sample_helio_au.y().to(); - - std::printf(" Direction x=%.1f y=%.1f z=%.1f\n", axis_x.x, axis_x.y, axis_x.z); - std::printf(" Position x=%.3f AU y=%.3f AU z=%.3f AU\n", - sample_helio_au.x().value(), sample_helio_au.y().value(), sample_helio_au.z().value()); - std::printf(" Same position in km x=%.2f y=%.2f\n\n", x_km.value(), y_km.value()); -} - -static void ephemeris_typed_example() { - std::printf("5) Typed ephemeris coordinates\n"); - - auto jd = JulianDate::J2000(); - auto earth = ephemeris::earth_heliocentric(jd); // cartesian::position::EclipticMeanJ2000 - auto moon = ephemeris::moon_geocentric(jd); // cartesian::position::MoonGeocentric - - std::printf(" Earth heliocentric (AU) x=%.8f y=%.8f z=%.8f\n", - earth.x().value(), earth.y().value(), earth.z().value()); - std::printf(" Moon geocentric (km) x=%.3f y=%.3f z=%.3f\n\n", - moon.x().value(), moon.y().value(), moon.z().value()); -} - -int main() { - std::printf("=== Coordinate Creation & Conversion Examples ===\n\n"); + auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto hor = vega_icrs.to_horizontal(jd, obs); + std::cout << "Vega az=" << std::setprecision(2) << hor.az().value() << " alt=" << hor.alt().value() << "\n"; - geodetic_and_ecef_example(); - spherical_direction_example(); - spherical_position_example(); - cartesian_and_units_example(); - ephemeris_typed_example(); + // Ephemeris quick values + auto earth = ephemeris::earth_heliocentric(jd); + std::cout << "Earth x=" << std::setprecision(6) << earth.x().value() << " AU\n"; - std::printf("Done.\n"); + std::cout << "Done.\n"; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 40c2038..578bc6b 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -8,118 +8,38 @@ */ #include -#include +#include +#include #include int main() { using namespace siderust; - using namespace siderust::frames; using namespace qtty::literals; - std::printf("=== siderust-cpp demo ===\n\n"); + std::cout << "=== siderust-cpp demo (concise) ===\n"; - // --- Time --- + // Time auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - std::printf("JD for 2026-07-15 22:00 UTC: %.6f\n", jd.value()); - std::printf("Julian centuries since J2000: %.10f\n", jd.julian_centuries()); + std::cout << "JD=" << std::fixed << std::setprecision(6) << jd.value() << " UTC=" << jd.to_utc() << "\n"; + // Sun altitude (deg) auto mjd = MJD::from_jd(jd); - std::printf("MJD: %.6f\n\n", mjd.value()); + qtty::Radian sun_alt = sun::altitude_at(ROQUE_DE_LOS_MUCHACHOS, mjd); + std::cout << "Sun alt=" << std::fixed << std::setprecision(2) << sun_alt.to().value() << " deg\n"; - // --- Observatory --- - auto obs = ROQUE_DE_LOS_MUCHACHOS; - std::printf("Roque de los Muchachos: lon=%.4f lat=%.4f h=%.0f m\n\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - - // --- Sun altitude --- - qtty::Radian sun_alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad (%.2f deg)\n\n", - sun_alt.value(), sun_alt.to().value()); - - // --- Star catalog --- + // Vega altitude const auto& vega = VEGA; - std::printf("Star: %s, d=%.2f ly, L=%.2f Lsun\n", - vega.name().c_str(), vega.distance_ly(), - vega.luminosity_solar()); - - // --- Star altitude --- - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad (%.2f deg)\n\n", - star_alt.value(), star_alt.to().value()); - - // ================================================================= - // TYPED COORDINATE & EPHEMERIS API - // ================================================================= - std::printf("--- Typed Coordinate API ---\n\n"); - - // Compile-time typed ICRS direction - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - - // Template-targeted transform: ICRS → EclipticMeanJ2000 - auto ecl = vega_icrs.to_frame(jd); - std::printf("Typed ICRS (%.4f, %.4f) -> EclMeanJ2000 (%.4f, %.4f)\n", - vega_icrs.ra().value(), vega_icrs.dec().value(), - ecl.lon().value(), ecl.lat().value()); - - // Shorthand .to<>() syntax - auto eq_j2000 = vega_icrs.to(jd); - std::printf("Typed ICRS -> EquatorialJ2000 (%.4f, %.4f)\n", - eq_j2000.ra().value(), eq_j2000.dec().value()); - - // Horizontal transform - auto hor = vega_icrs.to_horizontal(jd, obs); - std::printf("Typed Horizontal: az=%.4f alt=%.4f deg\n\n", - hor.az().value(), hor.al().value()); - - // Roundtrip: ICRS → Ecliptic → ICRS - auto back = ecl.to_frame(jd); - std::printf("Roundtrip: (%.6f, %.6f) -> (%.6f, %.6f) -> (%.6f, %.6f)\n", - vega_icrs.ra().value(), vega_icrs.dec().value(), - ecl.lon().value(), ecl.lat().value(), - back.ra().value(), back.dec().value()); - - // qtty unit-safe angle conversion - qtty::Radian ra_rad = vega_icrs.ra().to(); - std::printf("Vega RA: %.6f deg = %.6f rad\n\n", vega_icrs.ra().value(), ra_rad.value()); - - // --- Typed Ephemeris --- - std::printf("--- Typed Ephemeris ---\n\n"); + auto vega_alt = star_altitude::altitude_at(vega, ROQUE_DE_LOS_MUCHACHOS, mjd); + std::cout << "Vega alt=" << std::setprecision(2) << vega_alt.to().value() << " deg\n"; + // Simple ephemeris values auto earth = ephemeris::earth_heliocentric(jd); - std::printf("Earth heliocentric (typed AU): (%.8f, %.8f, %.8f)\n", - earth.x().value(), earth.y().value(), earth.z().value()); - - // Unit conversion: AU → km - qtty::Kilometer x_km = earth.comp_x.to(); - qtty::Kilometer y_km = earth.comp_y.to(); - std::printf("Earth heliocentric (km): (%.2f, %.2f, ...)\n\n", - x_km.value(), y_km.value()); + std::cout << "Earth (AU) x=" << std::setprecision(6) << earth.x().value() << " y=" << earth.y().value() << "\n"; auto moon = ephemeris::moon_geocentric(jd); - std::printf("Moon geocentric (typed km): (%.2f, %.2f, %.2f)\n", - moon.x().value(), moon.y().value(), moon.z().value()); - auto moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); - std::printf("Moon distance: %.2f km\n\n", moon_r); - - // --- Planets --- - auto mars_data = MARS; - std::printf("Mars: mass=%.4e kg, radius=%.2f km\n", - mars_data.mass_kg, mars_data.radius_km); - std::printf(" orbit: a=%.6f AU, e=%.6f\n\n", - mars_data.orbit.semi_major_axis_au, - mars_data.orbit.eccentricity); - - // --- Night periods (sun below -18°) --- - auto night_start = mjd; - auto night_end = mjd + 1.0; - auto nights = sun::below_threshold(obs, night_start, night_end, -18.0_deg); - std::printf("Astronomical night periods (sun < -18 deg):\n"); - for (auto& p : nights) { - std::printf(" MJD %.6f – %.6f (%.2f hours)\n", - p.start_mjd(), p.end_mjd(), - p.duration_days() * 24.0); - } + double moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); + std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_r << " km\n"; - std::printf("\nDone.\n"); + std::cout << "Done.\n"; return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index bf82f2f..d34a7ea 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -9,71 +9,28 @@ */ #include - +#include +#include #include -#include - -using namespace siderust; - -template -static double norm3(const PosT& p) { - const double x = p.x().value(); - const double y = p.y().value(); - const double z = p.z().value(); - return std::sqrt(x * x + y * y + z * z); -} - -static void print_planet(const char* name, const Planet& p) { - std::printf("%-8s mass=%.4e kg radius=%.1f km a=%.6f AU e=%.6f i=%.3f deg\n", - name, - p.mass_kg, - p.radius_km, - p.orbit.semi_major_axis_au, - p.orbit.eccentricity, - p.orbit.inclination_deg); -} int main() { - std::printf("=== Solar System Bodies Example ===\n\n"); + using namespace siderust; + using namespace qtty::literals; + + std::cout << "=== solar_system_bodies_example (concise) ===\n"; auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::printf("Epoch JD: %.6f\n\n", jd.value()); + std::cout << "Epoch JD=" << std::fixed << std::setprecision(6) << jd.value() << "\n"; - auto sun_bary = ephemeris::sun_barycentric(jd); - auto earth_bary = ephemeris::earth_barycentric(jd); auto earth_helio = ephemeris::earth_heliocentric(jd); - auto moon_geo = ephemeris::moon_geocentric(jd); - - std::printf("Sun barycentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - sun_bary.x().value(), sun_bary.y().value(), sun_bary.z().value()); - std::printf("Earth barycentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - earth_bary.x().value(), earth_bary.y().value(), earth_bary.z().value()); - std::printf("Earth heliocentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - earth_helio.x().value(), earth_helio.y().value(), earth_helio.z().value()); - std::printf("Moon geocentric (EclipticMeanJ2000, km):\n"); - std::printf(" x=%.3f y=%.3f z=%.3f\n\n", - moon_geo.x().value(), moon_geo.y().value(), moon_geo.z().value()); - - const double earth_sun_au = norm3(earth_helio); - const double moon_dist_km = norm3(moon_geo); - std::printf("Earth-Sun distance: %.6f AU\n", earth_sun_au); - std::printf("Moon distance from geocenter: %.2f km\n", moon_dist_km); + std::cout << "Earth heliocentric x=" << std::setprecision(6) << earth_helio.x().value() << " AU\n"; - const qtty::Kilometer earth_x_km = earth_helio.x().to(); - std::printf("Earth heliocentric x component: %.2f km\n\n", earth_x_km.value()); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + moon_geo.y().value() * moon_geo.y().value() + moon_geo.z().value() * moon_geo.z().value()); + std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_dist << " km\n"; - std::printf("Planet catalog (static properties):\n"); - print_planet("Mercury", MERCURY); - print_planet("Venus", VENUS); - print_planet("Earth", EARTH); - print_planet("Mars", MARS); - print_planet("Jupiter", JUPITER); - print_planet("Saturn", SATURN); - print_planet("Uranus", URANUS); - print_planet("Neptune", NEPTUNE); + // Print a couple of planets concisely + std::cout << "Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU Earth a=" << EARTH.orbit.semi_major_axis_au << " AU\n"; return 0; } diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp new file mode 100644 index 0000000..8c3baef --- /dev/null +++ b/include/siderust/azimuth.hpp @@ -0,0 +1,353 @@ +#pragma once + +/** + * @file azimuth.hpp + * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS directions. + * + * Wraps siderust-ffi's azimuth API with exception-safe C++ types and + * RAII-managed output arrays. + * + * ### Covered computations + * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | in_azimuth_range | + * |---------|:----------:|:-----------------:|:---------------:|:----------------:| + * | Sun | ✓ | ✓ | ✓ | ✓ | + * | Moon | ✓ | ✓ | ✓ | ✓ | + * | Star | ✓ | ✓ | – | – | + * | ICRS | ✓ | – | – | – | + */ + +#include "altitude.hpp" +#include "bodies.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include + +namespace siderust { + +// ============================================================================ +// Azimuth event types +// ============================================================================ + +/** + * @brief Distinguishes azimuth extrema: northernmost or southernmost bearing. + */ +enum class AzimuthExtremumKind : int32_t { + Max = 0, ///< Northernmost (or easternmost) direction reached by the body. + Min = 1, ///< Southernmost (or westernmost) direction reached by the body. +}; + +/** + * @brief An azimuth bearing-crossing event. + */ +struct AzimuthCrossingEvent { + MJD time; ///< Epoch of the crossing (MJD). + CrossingDirection direction; ///< Whether the azimuth is increasing or decreasing. + + static AzimuthCrossingEvent from_c(const siderust_azimuth_crossing_event_t& c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } +}; + +/** + * @brief An azimuth extremum event. + */ +struct AzimuthExtremum { + MJD time; ///< Epoch of the extremum (MJD). + qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). + AzimuthExtremumKind kind; ///< Maximum or minimum. + + static AzimuthExtremum from_c(const siderust_azimuth_extremum_t& c) { + return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), + static_cast(c.kind)}; + } +}; + +// ============================================================================ +// Internal helpers +// ============================================================================ +namespace detail { + +inline std::vector az_crossings_from_c( + siderust_azimuth_crossing_event_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); + } + siderust_azimuth_crossings_free(ptr, count); + return result; +} + +inline std::vector az_extrema_from_c( + siderust_azimuth_extremum_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthExtremum::from_c(ptr[i])); + } + siderust_azimuth_extrema_free(ptr, count); + return result; +} + +} // namespace detail + +// ============================================================================ +// Sun azimuth +// ============================================================================ + +namespace sun { + +/** + * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), + "sun::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when the Sun crosses a given bearing. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_crossings( + obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "sun::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); +} + +/** + * @brief Find azimuth extrema (northernmost / southernmost) for the Sun. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_extrema( + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); +} + +/** + * @brief Find periods when the Sun's azimuth is within [min_bearing, max_bearing]. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const Period& window, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_in_azimuth_range( + obs.to_c(), window.c_inner(), + min_bearing.value(), max_bearing.value(), + opts.to_c(), &ptr, &count), + "sun::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +} + +} // namespace sun + +// ============================================================================ +// Moon azimuth +// ============================================================================ + +namespace moon { + +/** + * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), + "moon::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when the Moon crosses a given bearing. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_crossings( + obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "moon::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); +} + +/** + * @brief Find azimuth extrema (northernmost / southernmost) for the Moon. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_extrema( + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); +} + +/** + * @brief Find periods when the Moon's azimuth is within [min_bearing, max_bearing]. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const Period& window, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_in_azimuth_range( + obs.to_c(), window.c_inner(), + min_bearing.value(), max_bearing.value(), + opts.to_c(), &ptr, &count), + "moon::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +} + +} // namespace moon + +// ============================================================================ +// Star azimuth +// ============================================================================ + +namespace star_altitude { + +/** + * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Star& s, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_star_azimuth_at( + s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when a star crosses a given azimuth bearing. + */ +inline std::vector azimuth_crossings( + const Star& s, const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_azimuth_crossings( + s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "star_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(s, obs, Period(start, end), bearing, opts); +} + +} // namespace star_altitude + +// ============================================================================ +// ICRS direction azimuth +// ============================================================================ + +namespace icrs_altitude { + +/** + * @brief Compute azimuth (degrees, N-clockwise) for a fixed ICRS direction. + */ +inline qtty::Degree azimuth_at(const spherical::direction::ICRS& dir, + const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_icrs_azimuth_at( + dir.to_c(), + obs.to_c(), mjd.value(), &out), + "icrs_altitude::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Backward-compatible RA/Dec overload. + */ +inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, + const Geodetic& obs, const MJD& mjd) { + return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); +} + +} // namespace icrs_altitude + +} // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index cd94da9..58f551e 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -102,11 +102,11 @@ struct Direction { /// @name FFI interop /// @{ siderust_spherical_dir_t to_c() const { - return {azimuth_.value(), polar_.value(), frame_id()}; + return {polar_.value(), azimuth_.value(), frame_id()}; } static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.lon_deg, c.lat_deg); + return Direction(c.azimuth_deg, c.polar_deg); } /// @} @@ -129,7 +129,7 @@ struct Direction { siderust_spherical_dir_t out; check_status( siderust_spherical_dir_transform_frame( - azimuth_.value(), polar_.value(), + polar_.value(), azimuth_.value(), frames::FrameTraits::ffi_id, frames::FrameTraits::ffi_id, jd.value(), &out), @@ -158,7 +158,7 @@ struct Direction { siderust_spherical_dir_t out; check_status( siderust_spherical_dir_to_horizontal( - azimuth_.value(), polar_.value(), + polar_.value(), azimuth_.value(), frames::FrameTraits::ffi_id, jd.value(), observer.to_c(), &out), "Direction::to_horizontal"); diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp new file mode 100644 index 0000000..a75c15e --- /dev/null +++ b/include/siderust/lunar_phase.hpp @@ -0,0 +1,265 @@ +#pragma once + +/** + * @file lunar_phase.hpp + * @brief Lunar phase geometry, phase events, and illumination periods. + * + * Wraps siderust-ffi's lunar phase API with exception-safe C++ types and + * RAII-managed output arrays. + * + * All phase-geometry functions accept a Julian Date (siderust::JulianDate). + * Search windows use the regular MJD-based siderust::Period. + */ + +#include "altitude.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include + +namespace siderust { + +// ============================================================================ +// Phase enumerations +// ============================================================================ + +/** + * @brief Principal lunar phase kinds (new-moon quarter events). + */ +enum class PhaseKind : int32_t { + NewMoon = 0, + FirstQuarter = 1, + FullMoon = 2, + LastQuarter = 3, +}; + +/** + * @brief Descriptive moon phase labels (8 canonical phases). + */ +enum class MoonPhaseLabel : int32_t { + NewMoon = 0, + WaxingCrescent = 1, + FirstQuarter = 2, + WaxingGibbous = 3, + FullMoon = 4, + WaningGibbous = 5, + LastQuarter = 6, + WaningCrescent = 7, +}; + +// ============================================================================ +// Phase event / geometry types +// ============================================================================ + +/** + * @brief Geometric description of the Moon's phase at a point in time. + */ +struct MoonPhaseGeometry { + double phase_angle_rad; ///< Phase angle in [0, π], radians. + double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. + double elongation_rad; ///< Sun–Moon elongation, radians. + bool waxing; ///< True when the Moon is waxing. + + static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t& c) { + return {c.phase_angle_rad, c.illuminated_fraction, + c.elongation_rad, static_cast(c.waxing)}; + } +}; + +/** + * @brief A principal lunar phase event (new moon, first quarter, etc.). + */ +struct PhaseEvent { + MJD time; ///< Epoch of the event (MJD). + PhaseKind kind; ///< Which principal phase occurred. + + static PhaseEvent from_c(const siderust_phase_event_t& c) { + return {MJD(c.mjd), static_cast(c.kind)}; + } +}; + +// ============================================================================ +// Internal helpers +// ============================================================================ +namespace detail { + +inline std::vector phase_events_from_c( + siderust_phase_event_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(PhaseEvent::from_c(ptr[i])); + } + siderust_phase_events_free(ptr, count); + return result; +} + +/// Like periods_from_c but for tempoch_period_mjd_t* pointers (freed with +/// siderust_periods_free). +inline std::vector illum_periods_from_c( + tempoch_period_mjd_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; +} + +} // namespace detail + +// ============================================================================ +// Lunar phase namespace +// ============================================================================ + +namespace moon { + +/** + * @brief Compute geocentric Moon phase geometry at a Julian Date. + * + * @param jd Julian Date (e.g. `siderust::JulianDate(2451545.0)` for J2000.0). + */ +inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_geocentric(jd.value(), &out), + "moon::phase_geocentric"); + return MoonPhaseGeometry::from_c(out); +} + +/** + * @brief Compute topocentric Moon phase geometry at a Julian Date. + * + * @param jd Julian Date. + * @param site Observer geodetic coordinates. + */ +inline MoonPhaseGeometry phase_topocentric(const JulianDate& jd, + const Geodetic& site) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), + "moon::phase_topocentric"); + return MoonPhaseGeometry::from_c(out); +} + +/** + * @brief Determine the descriptive phase label for a given geometry. + * + * @param geom Moon phase geometry (as returned by phase_geocentric / phase_topocentric). + */ +inline MoonPhaseLabel phase_label(const MoonPhaseGeometry& geom) { + siderust_moon_phase_geometry_t c{geom.phase_angle_rad, + geom.illuminated_fraction, + geom.elongation_rad, + static_cast(geom.waxing)}; + siderust_moon_phase_label_t out{}; + check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); + return static_cast(out); +} + +/** + * @brief Find principal phase events (new moon, quarters, full moon) in a window. + * + * @param window MJD search window. + * @param opts Search tolerances (optional). + */ +inline std::vector find_phase_events( + const Period& window, const SearchOptions& opts = {}) { + siderust_phase_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_find_phase_events( + window.c_inner(), opts.to_c(), &ptr, &count), + "moon::find_phase_events"); + return detail::phase_events_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector find_phase_events( + const MJD& start, const MJD& end, const SearchOptions& opts = {}) { + return find_phase_events(Period(start, end), opts); +} + +/** + * @brief Find periods when illuminated fraction is ≥ k_min. + * + * @param window MJD search window. + * @param k_min Minimum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_above( + const Period& window, double k_min, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_above( + window.c_inner(), k_min, opts.to_c(), &ptr, &count), + "moon::illumination_above"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_above( + const MJD& start, const MJD& end, double k_min, + const SearchOptions& opts = {}) { + return illumination_above(Period(start, end), k_min, opts); +} + +/** + * @brief Find periods when illuminated fraction is ≤ k_max. + * + * @param window MJD search window. + * @param k_max Maximum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_below( + const Period& window, double k_max, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_below( + window.c_inner(), k_max, opts.to_c(), &ptr, &count), + "moon::illumination_below"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_below( + const MJD& start, const MJD& end, double k_max, + const SearchOptions& opts = {}) { + return illumination_below(Period(start, end), k_max, opts); +} + +/** + * @brief Find periods when illuminated fraction is within [k_min, k_max]. + * + * @param window MJD search window. + * @param k_min Minimum illuminated fraction in [0, 1]. + * @param k_max Maximum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_range( + const Period& window, double k_min, double k_max, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_range( + window.c_inner(), k_min, k_max, opts.to_c(), &ptr, &count), + "moon::illumination_range"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_range( + const MJD& start, const MJD& end, double k_min, double k_max, + const SearchOptions& opts = {}) { + return illumination_range(Period(start, end), k_min, k_max, opts); +} + +} // namespace moon + +} // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 5cf0df1..1ae07d4 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -27,11 +27,14 @@ */ #include "altitude.hpp" +#include "azimuth.hpp" #include "bodies.hpp" #include "centers.hpp" #include "coordinates.hpp" #include "ephemeris.hpp" #include "ffi_core.hpp" #include "frames.hpp" +#include "lunar_phase.hpp" #include "observatories.hpp" +#include "target.hpp" #include "time.hpp" diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp new file mode 100644 index 0000000..ae77df0 --- /dev/null +++ b/include/siderust/target.hpp @@ -0,0 +1,252 @@ +#pragma once + +/** + * @file target.hpp + * @brief RAII C++ wrapper for an siderust Target (fixed ICRS pointing). + * + * A `Target` represents a fixed celestial direction (RA, Dec at a given epoch) + * and exposes altitude and azimuth computations via the same observer/window + * API as the sun/moon/star helpers in altitude.hpp and azimuth.hpp. + * + * Proper motion is stored for future use but presently not applied during + * altitude/azimuth queries (epoch-fixed direction is used throughout). + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include +#include + +namespace siderust { + +/** + * @brief Move-only RAII handle for a siderust target direction. + * + * ### Example + * @code + * siderust::Target vega(279.2348, +38.7836, 2451545.0); // Vega at J2000 + * auto alt = vega.altitude_at(obs, now); + * @endcode + */ +class Target { + public: + // ------------------------------------------------------------------ + // Construction / destruction + // ------------------------------------------------------------------ + + /** + * @brief Create a target from ICRS [RA, Dec] and an epoch. + * + * @param ra_deg Right ascension, degrees [0, 360). + * @param dec_deg Declination, degrees [−90, +90]. + * @param epoch_jd Julian Date of the coordinate epoch (default J2000.0). + */ + explicit Target(double ra_deg, double dec_deg, double epoch_jd = 2451545.0) { + SiderustTarget* h = nullptr; + check_status(siderust_target_create(ra_deg, dec_deg, epoch_jd, &h), + "Target::Target"); + handle_ = h; + } + + ~Target() { + if (handle_) { + siderust_target_free(handle_); + handle_ = nullptr; + } + } + + /// Move constructor. + Target(Target&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + /// Move assignment. + Target& operator=(Target&& other) noexcept { + if (this != &other) { + if (handle_) { + siderust_target_free(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + // Prevent copying (the handle has unique ownership). + Target(const Target&) = delete; + Target& operator=(const Target&) = delete; + + // ------------------------------------------------------------------ + // Coordinate accessors + // ------------------------------------------------------------------ + + /// Right ascension of the target (degrees). + double ra_deg() const { + double out{}; + check_status(siderust_target_ra_deg(handle_, &out), "Target::ra_deg"); + return out; + } + + /// Declination of the target (degrees). + double dec_deg() const { + double out{}; + check_status(siderust_target_dec_deg(handle_, &out), "Target::dec_deg"); + return out; + } + + /// Epoch of the coordinates (Julian Date). + double epoch_jd() const { + double out{}; + check_status(siderust_target_epoch_jd(handle_, &out), "Target::epoch_jd"); + return out; + } + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude (degrees) at a given MJD instant. + */ + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const { + double out{}; + check_status(siderust_target_altitude_at( + handle_, obs.to_c(), mjd.value(), &out), + "Target::altitude_at"); + return qtty::Degree(out); + } + + /** + * @brief Find periods when the target is above a threshold altitude. + */ + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_above_threshold( + handle_, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::above_threshold"); + return detail_periods_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector above_threshold( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return above_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + siderust_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_crossings( + handle_, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::crossings"); + return detail::crossings_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return crossings(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find culmination (local altitude extremum) events. + */ + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const { + siderust_culmination_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_culminations( + handle_, obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "Target::culminations"); + return detail::culminations_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector culminations( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) const { + return culminations(obs, Period(start, end), opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const { + double out{}; + check_status(siderust_target_azimuth_at( + handle_, obs.to_c(), mjd.value(), &out), + "Target::azimuth_at"); + return qtty::Degree(out); + } + + /** + * @brief Find epochs when the target crosses a given azimuth bearing. + */ + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_azimuth_crossings( + handle_, obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "Target::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) const { + return azimuth_crossings(obs, Period(start, end), bearing, opts); + } + + /// Access the underlying C handle (advanced use). + const SiderustTarget* c_handle() const { return handle_; } + + private: + SiderustTarget* handle_ = nullptr; + + /// Build a Period vector from a tempoch_period_mjd_t* array. + static std::vector detail_periods_from_c( + tempoch_period_mjd_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; + } +}; + +} // namespace siderust diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index 351af5a..ddef029 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -13,9 +13,10 @@ namespace siderust { -using UTC = tempoch::UTC; -using JulianDate = tempoch::JulianDate; -using MJD = tempoch::MJD; -using Period = tempoch::Period; +using CivilTime = tempoch::CivilTime; +using UTC = tempoch::UTC; // alias for CivilTime +using JulianDate = tempoch::JulianDate; // Time +using MJD = tempoch::MJD; // Time +using Period = tempoch::Period; } // namespace siderust diff --git a/qtty-cpp b/qtty-cpp index 953ebe1..3612a8f 160000 --- a/qtty-cpp +++ b/qtty-cpp @@ -1 +1 @@ -Subproject commit 953ebe15bcd6f1b929d4516970e5127e2e1ad953 +Subproject commit 3612a8ff03f7290ea7fe53df897f10aa5716582f diff --git a/siderust b/siderust index 283032a..fe37bfd 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 283032a541a8bbcc46622e6a06f807717438f998 +Subproject commit fe37bfd5f9a878bf732681749ec7cdf4ef68e5c7 diff --git a/tempoch-cpp b/tempoch-cpp index 39a5e85..0b27c11 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit 39a5e8557e2382d47dcb3ebe01a3e3237f8a94e5 +Subproject commit 0b27c11cdc03fa016e3545753d3aaab1520ff576 diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 8ad505a..cc24154 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -16,7 +16,7 @@ class AltitudeTest : public ::testing::Test { void SetUp() override { obs = ROQUE_DE_LOS_MUCHACHOS; start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); - end_ = start + 1.0; // 24 hours + end_ = start + qtty::Day(1.0); // 24 hours window = Period(start, end_); } }; @@ -37,7 +37,7 @@ TEST_F(AltitudeTest, SunAboveThreshold) { auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); EXPECT_GT(periods.size(), 0u); for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -47,7 +47,7 @@ TEST_F(AltitudeTest, SunBelowThreshold) { // In July at La Palma, astronomical night may be short but should exist // (or possibly not if too close to solstice — accept 0+) for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -67,7 +67,7 @@ TEST_F(AltitudeTest, SunAltitudePeriods) { // Find periods when sun is between -6° and 0° (civil twilight) auto periods = sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -85,7 +85,7 @@ TEST_F(AltitudeTest, MoonAboveThreshold) { auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); // Moon may or may not be above horizon for this date; just no crash for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } diff --git a/tests/test_time.cpp b/tests/test_time.cpp index 395814c..a2454c3 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -31,8 +31,8 @@ TEST(Time, JulianDateRoundtripUtc) { TEST(Time, JulianDateArithmetic) { auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + 365.25; - EXPECT_NEAR(jd2 - jd1, 365.25, 1e-10); + auto jd2 = jd1 + qtty::Day(365.25); + EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); } TEST(Time, JulianCenturies) { @@ -62,24 +62,74 @@ TEST(Time, MjdRoundtrip) { // ============================================================================ TEST(Time, PeriodDuration) { - Period p(60200.0, 60201.0); - EXPECT_NEAR(p.duration_days(), 1.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } TEST(Time, PeriodIntersection) { - Period a(60200.0, 60202.0); - Period b(60201.0, 60203.0); + Period a(MJD(60200.0), MJD(60202.0)); + Period b(MJD(60201.0), MJD(60203.0)); auto c = a.intersection(b); - EXPECT_NEAR(c.start_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(c.end_mjd(), 60202.0, 1e-10); + EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); + EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); } TEST(Time, PeriodNoIntersection) { - Period a(60200.0, 60201.0); - Period b(60202.0, 60203.0); + Period a(MJD(60200.0), MJD(60201.0)); + Period b(MJD(60202.0), MJD(60203.0)); EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); } TEST(Time, PeriodInvalidThrows) { - EXPECT_THROW(Period(60203.0, 60200.0), tempoch::InvalidPeriodError); + EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); +} + +// ============================================================================ +// Typed-quantity (_qty) methods +// ============================================================================ + +TEST(Time, JulianCenturiesQty) { + auto jd = JulianDate::J2000(); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 0.0, 1e-10); + EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); +} + +TEST(Time, JulianCenturiesQtyNonZero) { + // 36525 days ≈ 1 Julian century + auto jd = JulianDate(2451545.0 + 36525.0); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 1.0, 1e-10); +} + +TEST(Time, ArithmeticWithHours) { + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Hour(24.0); + EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); +} + +TEST(Time, ArithmeticWithMinutes) { + auto mjd1 = MJD(60200.0); + auto mjd2 = mjd1 + qtty::Minute(1440.0); + EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); +} + +TEST(Time, SubtractQuantityHours) { + auto jd1 = JulianDate(2451546.0); + auto jd2 = jd1 - qtty::Hour(12.0); + EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); +} + +TEST(Time, DifferenceConvertible) { + auto jd1 = JulianDate(2451545.0); + auto jd2 = JulianDate(2451546.0); + auto diff = jd2 - jd1; + auto hours = diff.to(); + EXPECT_NEAR(hours.value(), 24.0, 1e-10); +} + +TEST(Time, PeriodDurationInMinutes) { + Period p(MJD(60200.0), MJD(60200.5)); + auto min = p.duration(); + EXPECT_NEAR(min.value(), 720.0, 1e-6); } From 7054e73fa4fa624dea579492a15f775b9932b2c4 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:26:30 +0100 Subject: [PATCH 2/6] feat: implement BodyTarget and StarTarget classes for celestial object tracking --- CMakeLists.txt | 2 +- include/siderust/azimuth.hpp | 36 ++++ include/siderust/body_target.hpp | 298 +++++++++++++++++++++++++++++++ include/siderust/ffi_core.hpp | 11 ++ include/siderust/lunar_phase.hpp | 39 ++++ include/siderust/siderust.hpp | 2 + include/siderust/star_target.hpp | 95 ++++++++++ include/siderust/target.hpp | 44 ++++- include/siderust/trackable.hpp | 119 ++++++++++++ siderust | 2 +- tests/test_bodies.cpp | 124 +++++++++++++ 11 files changed, 763 insertions(+), 9 deletions(-) create mode 100644 include/siderust/body_target.hpp create mode 100644 include/siderust/star_target.hpp create mode 100644 include/siderust/trackable.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 55eb949..441a999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ endif() # --------------------------------------------------------------------------- # Platform-specific library paths # --------------------------------------------------------------------------- -set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/siderust-ffi/target/release) +set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/target/release) if(APPLE) set(SIDERUST_LIBRARY_PATH ${SIDERUST_ARTIFACT_DIR}/libsiderust_ffi.dylib) diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 8c3baef..7f1d4f7 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -348,6 +348,42 @@ inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); } +/** + * @brief Find epochs when an ICRS direction crosses a given azimuth bearing. + */ +inline std::vector azimuth_crossings( + const spherical::direction::ICRS& dir, + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_azimuth_crossings( + dir.to_c(), obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "icrs_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible RA/Dec overload. + */ +inline std::vector azimuth_crossings( + qtty::Degree ra, qtty::Degree dec, + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, bearing, opts); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const spherical::direction::ICRS& dir, + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); +} + } // namespace icrs_altitude } // namespace siderust diff --git a/include/siderust/body_target.hpp b/include/siderust/body_target.hpp new file mode 100644 index 0000000..cc39e2a --- /dev/null +++ b/include/siderust/body_target.hpp @@ -0,0 +1,298 @@ +#pragma once + +/** + * @file body_target.hpp + * @brief Trackable wrapper for solar-system bodies. + * + * `BodyTarget` implements the `Trackable` interface for any solar-system + * body identified by the `Body` enum. It dispatches altitude and azimuth + * computations through the siderust-ffi `siderust_body_*` functions, which + * in turn use VSOP87 (planets), specialised engines (Sun/Moon), or + * Meeus/Williams series (Pluto) for ephemeris. + * + * ### Example + * @code + * using namespace siderust; + * BodyTarget mars(Body::Mars); + * qtty::Degree alt = mars.altitude_at(obs, now); + * + * // Polymorphic usage + * std::vector> targets; + * targets.push_back(std::make_unique(Body::Sun)); + * targets.push_back(std::make_unique(Body::Jupiter)); + * for (const auto& t : targets) { + * std::cout << t->altitude_at(obs, now).value() << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "ffi_core.hpp" +#include "trackable.hpp" + +namespace siderust { + +// ============================================================================ +// Body enum +// ============================================================================ + +/** + * @brief Identifies a solar-system body for generic altitude/azimuth dispatch. + * + * Maps 1:1 to the FFI `SiderustBody` discriminant. + */ +enum class Body : int32_t { + Sun = SIDERUST_BODY_SUN, + Moon = SIDERUST_BODY_MOON, + Mercury = SIDERUST_BODY_MERCURY, + Venus = SIDERUST_BODY_VENUS, + Mars = SIDERUST_BODY_MARS, + Jupiter = SIDERUST_BODY_JUPITER, + Saturn = SIDERUST_BODY_SATURN, + Uranus = SIDERUST_BODY_URANUS, + Neptune = SIDERUST_BODY_NEPTUNE, +}; + +// ============================================================================ +// Free functions in body:: namespace +// ============================================================================ + +namespace body { + +/** + * @brief Compute a body's altitude (radians) at a given MJD instant. + */ +inline qtty::Radian altitude_at(Body b, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_body_altitude_at( + static_cast(b), obs.to_c(), mjd.value(), &out), + "body::altitude_at"); + return qtty::Radian(out); +} + +/** + * @brief Find periods when a body is above a threshold altitude. + */ +inline std::vector above_threshold( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_above_threshold( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::above_threshold"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Find periods when a body is below a threshold altitude. + */ +inline std::vector below_threshold( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_below_threshold( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::below_threshold"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Find threshold-crossing events for a body. + */ +inline std::vector crossings( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + siderust_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_crossings( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::crossings"); + return detail::crossings_from_c(ptr, count); +} + +/** + * @brief Find culmination events for a body. + */ +inline std::vector culminations( + Body b, const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_culmination_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_culminations( + static_cast(b), obs.to_c(), + window.c_inner(), + opts.to_c(), &ptr, &count), + "body::culminations"); + return detail::culminations_from_c(ptr, count); +} + +/** + * @brief Find periods when a body's altitude is within [min, max]. + */ +inline std::vector altitude_periods( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree min_alt, qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_altitude_periods( + static_cast(b), q, &ptr, &count), + "body::altitude_periods"); + return detail::periods_from_c(ptr, count); +} + +} // namespace body + +namespace body { + +// ── Azimuth free functions ────────────────────────────────────────────── + +/** + * @brief Compute a body's azimuth (radians) at a given MJD instant. + */ +inline qtty::Radian azimuth_at(Body b, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_body_azimuth_at( + static_cast(b), obs.to_c(), mjd.value(), &out), + "body::azimuth_at"); + return qtty::Radian(out); +} + +/** + * @brief Find azimuth-bearing crossing events for a body. + */ +inline std::vector azimuth_crossings( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_crossings( + static_cast(b), obs.to_c(), + window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "body::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Find azimuth extrema (northernmost/southernmost bearing) for a body. + */ +inline std::vector azimuth_extrema( + Body b, const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_extrema( + static_cast(b), obs.to_c(), + window.c_inner(), + opts.to_c(), &ptr, &count), + "body::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Find periods when a body's azimuth is within [min, max]. + */ +inline std::vector in_azimuth_range( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree min, qtty::Degree max, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_in_azimuth_range( + static_cast(b), obs.to_c(), + window.c_inner(), min.value(), max.value(), + opts.to_c(), &ptr, &count), + "body::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +} // namespace body + +// ============================================================================ +// BodyTarget — Trackable adapter for solar-system bodies +// ============================================================================ + +/** + * @brief Trackable adapter for solar-system bodies. + * + * Wraps a `Body` enum value and dispatches all altitude/azimuth queries + * through the FFI `siderust_body_*` functions. + * + * `BodyTarget` is lightweight (holds a single enum value), copyable, and + * can be used directly or stored as `std::unique_ptr` for + * polymorphic dispatch. + */ +class BodyTarget : public Trackable { + public: + /** + * @brief Construct a BodyTarget for a given solar-system body. + * @param body The body to track. + */ + explicit BodyTarget(Body body) : body_(body) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { + auto rad = body::altitude_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::above_threshold(body_, obs, window, threshold, opts); + } + + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::below_threshold(body_, obs, window, threshold, opts); + } + + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::crossings(body_, obs, window, threshold, opts); + } + + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const override { + return body::culminations(body_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { + auto rad = body::azimuth_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const override { + return body::azimuth_crossings(body_, obs, window, bearing, opts); + } + + /// Access the underlying Body enum value. + Body body() const { return body_; } + + private: + Body body_; +}; + +} // namespace siderust diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index 94d8a9f..d673029 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -112,6 +112,17 @@ inline void check_tempoch_status(tempoch_status_t status, const char* operation) tempoch::check_status(status, operation); } +// ============================================================================ +// FFI version +// ============================================================================ + +/** + * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + patch). + */ +inline uint32_t ffi_version() { + return siderust_ffi_version(); +} + // ============================================================================ // Frame and Center Enums (C++ typed) // ============================================================================ diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index a75c15e..b031634 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -262,4 +262,43 @@ inline std::vector illumination_range( } // namespace moon +// ============================================================================ +// Convenience helpers (pure C++, no FFI) +// ============================================================================ + +/** + * @brief Get the illuminated fraction as a percentage [0, 100]. + */ +inline double illuminated_percent(const MoonPhaseGeometry& geom) { + return geom.illuminated_fraction * 100.0; +} + +/** + * @brief Check if a phase label describes a waxing moon. + */ +inline bool is_waxing(MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::WaxingCrescent: + case MoonPhaseLabel::FirstQuarter: + case MoonPhaseLabel::WaxingGibbous: + return true; + default: + return false; + } +} + +/** + * @brief Check if a phase label describes a waning moon. + */ +inline bool is_waning(MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::WaningGibbous: + case MoonPhaseLabel::LastQuarter: + case MoonPhaseLabel::WaningCrescent: + return true; + default: + return false; + } +} + } // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 1ae07d4..dfc22cb 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -29,6 +29,7 @@ #include "altitude.hpp" #include "azimuth.hpp" #include "bodies.hpp" +#include "body_target.hpp" #include "centers.hpp" #include "coordinates.hpp" #include "ephemeris.hpp" @@ -36,5 +37,6 @@ #include "frames.hpp" #include "lunar_phase.hpp" #include "observatories.hpp" +#include "star_target.hpp" #include "target.hpp" #include "time.hpp" diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp new file mode 100644 index 0000000..72e336a --- /dev/null +++ b/include/siderust/star_target.hpp @@ -0,0 +1,95 @@ +#pragma once + +/** + * @file star_target.hpp + * @brief Trackable adapter for Star objects. + * + * `StarTarget` wraps a `const Star&` and implements the `Trackable` + * interface by delegating to the `star_altitude::` and `star_altitude::` + * namespace free functions. + * + * ### Example + * @code + * siderust::StarTarget vega_target(siderust::VEGA); + * auto alt = vega_target.altitude_at(obs, now); + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "bodies.hpp" +#include "trackable.hpp" + +namespace siderust { + +/** + * @brief Trackable adapter wrapping a `const Star&`. + * + * The referenced `Star` must outlive the `StarTarget`. Typically used with + * the pre-built catalog stars (e.g. `VEGA`, `SIRIUS`) which are `inline const` + * globals and live for the entire program. + */ +class StarTarget : public Trackable { + public: + /** + * @brief Wrap a Star reference as a Trackable. + * @param star Reference to a Star. Must outlive this adapter. + */ + explicit StarTarget(const Star& star) : star_(star) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { + // star_altitude::altitude_at returns Radian; convert to Degree + auto rad = star_altitude::altitude_at(star_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::above_threshold(star_, obs, window, threshold, opts); + } + + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::below_threshold(star_, obs, window, threshold, opts); + } + + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::crossings(star_, obs, window, threshold, opts); + } + + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const override { + return star_altitude::culminations(star_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { + return star_altitude::azimuth_at(star_, obs, mjd); + } + + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const override { + return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); + } + + /// Access the underlying Star reference. + const Star& star() const { return star_; } + + private: + const Star& star_; +}; + +} // namespace siderust diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index ae77df0..640a1d8 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -17,6 +17,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include "trackable.hpp" #include #include @@ -31,7 +32,7 @@ namespace siderust { * auto alt = vega.altitude_at(obs, now); * @endcode */ -class Target { +class Target : public Trackable { public: // ------------------------------------------------------------------ // Construction / destruction @@ -111,7 +112,7 @@ class Target { /** * @brief Compute altitude (degrees) at a given MJD instant. */ - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const { + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_altitude_at( handle_, obs.to_c(), mjd.value(), &out), @@ -124,7 +125,7 @@ class Target { */ std::vector above_threshold( const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const { + qtty::Degree threshold, const SearchOptions& opts = {}) const override { tempoch_period_mjd_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_above_threshold( @@ -143,12 +144,41 @@ class Target { return above_threshold(obs, Period(start, end), threshold, opts); } + /** + * @brief Find periods when the target is below a threshold altitude. + */ + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + // Target wraps an ICRS direction; use icrs_below_threshold FFI. + siderust_spherical_dir_t dir_c{}; + dir_c.polar_deg = dec_deg(); + dir_c.azimuth_deg = ra_deg(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir_c, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::below_threshold"); + return detail_periods_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector below_threshold( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return below_threshold(obs, Period(start, end), threshold, opts); + } + /** * @brief Find threshold-crossing events (rising / setting). */ std::vector crossings( const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const { + qtty::Degree threshold, const SearchOptions& opts = {}) const override { siderust_crossing_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_crossings( @@ -172,7 +202,7 @@ class Target { */ std::vector culminations( const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const { + const SearchOptions& opts = {}) const override { siderust_culmination_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_culminations( @@ -198,7 +228,7 @@ class Target { /** * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. */ - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const { + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_azimuth_at( handle_, obs.to_c(), mjd.value(), &out), @@ -211,7 +241,7 @@ class Target { */ std::vector azimuth_crossings( const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const { + qtty::Degree bearing, const SearchOptions& opts = {}) const override { siderust_azimuth_crossing_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_azimuth_crossings( diff --git a/include/siderust/trackable.hpp b/include/siderust/trackable.hpp new file mode 100644 index 0000000..5aefe37 --- /dev/null +++ b/include/siderust/trackable.hpp @@ -0,0 +1,119 @@ +#pragma once + +/** + * @file trackable.hpp + * @brief Abstract base class for trackable celestial objects. + * + * `Trackable` defines a polymorphic interface for any celestial object + * whose altitude and azimuth can be computed at an observer location. + * Implementations include: + * + * - **Target** — fixed ICRS direction (RA/Dec) + * - **StarTarget** — adapter for `Star` catalog objects + * - **BodyTarget** — solar-system bodies (Sun, Moon, planets, Pluto) + * + * Use `std::unique_ptr` to hold heterogeneous collections of + * trackable objects. + * + * ### Example + * @code + * auto sun = std::make_unique(siderust::Body::Sun); + * qtty::Degree alt = sun->altitude_at(obs, now); + * + * // Polymorphic usage + * std::vector> targets; + * targets.push_back(std::move(sun)); + * targets.push_back(std::make_unique(VEGA)); + * for (const auto& t : targets) { + * std::cout << t->altitude_at(obs, now).value() << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "time.hpp" +#include +#include + +namespace siderust { + +/** + * @brief Abstract interface for any object whose altitude/azimuth can be computed. + * + * This class defines the common API shared by all trackable celestial objects. + * Implementations must provide altitude_at and azimuth_at at minimum; the + * remaining methods have default implementations that throw if not overridden. + */ +class Trackable { + public: + virtual ~Trackable() = default; + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude at a given MJD instant. + * + * The return unit varies by implementation (radians for sun/moon/star, + * degrees for Target/BodyTarget). Check the concrete class documentation. + * + * @note For BodyTarget, returns radians; for Target, returns degrees. + */ + virtual qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const = 0; + + /** + * @brief Find periods when the object is above a threshold altitude. + */ + virtual std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find periods when the object is below a threshold altitude. + */ + virtual std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + virtual std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find culmination (local altitude extremum) events. + */ + virtual std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const = 0; + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + virtual qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const = 0; + + /** + * @brief Find epochs when the object crosses a given azimuth bearing. + */ + virtual std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const = 0; + + // Non-copyable, non-movable from base + Trackable() = default; + Trackable(const Trackable&) = delete; + Trackable& operator=(const Trackable&) = delete; + Trackable(Trackable&&) = default; + Trackable& operator=(Trackable&&) = default; +}; + +} // namespace siderust diff --git a/siderust b/siderust index fe37bfd..17d986f 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit fe37bfd5f9a878bf732681749ec7cdf4ef68e5c7 +Subproject commit 17d986fd5d1c9c258df50ce66f36fc524266d6e4 diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 755e69b..530617e 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -83,3 +83,127 @@ TEST(Bodies, AllPlanets) { EXPECT_GT(URANUS.mass_kg, 0.0); EXPECT_GT(NEPTUNE.mass_kg, 0.0); } + +// ============================================================================ +// BodyTarget — generic solar-system body via Trackable polymorphism +// ============================================================================ + +TEST(Bodies, BodyTargetSunAltitude) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = sun.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, BodyTargetMarsAltitude) { + BodyTarget mars(Body::Mars); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = mars.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, BodyTargetAllBodiesAltitude) { + auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM + auto mjd = MJD(60000.5); + std::vector all = { + Body::Sun, Body::Moon, Body::Mercury, Body::Venus, + Body::Mars, Body::Jupiter, Body::Saturn, Body::Uranus, Body::Neptune + }; + for (auto b : all) { + BodyTarget bt(b); + auto alt = bt.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} + +TEST(Bodies, BodyTargetAzimuth) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = sun.azimuth_at(obs, mjd); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); +} + +TEST(Bodies, BodyTargetJupiterAzimuth) { + BodyTarget jup(Body::Jupiter); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = jup.azimuth_at(obs, mjd); + EXPECT_TRUE(std::isfinite(az.value())); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); +} + +TEST(Bodies, BodyTargetAboveThreshold) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto window = Period(MJD(60000.0), MJD(60001.0)); + auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); + // Sun should be above horizon for some portion of the day + EXPECT_GT(periods.size(), 0u); +} + +TEST(Bodies, BodyTargetPolymorphic) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Mars)); + + for (const auto& t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} + +TEST(Bodies, BodyNamespaceAltitudeAt) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::altitude_at(Body::Saturn, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); +} + +TEST(Bodies, BodyNamespaceAzimuthAt) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::azimuth_at(Body::Venus, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); + EXPECT_GE(rad.value(), 0.0); +} + +// ============================================================================ +// StarTarget — Trackable adapter for Star +// ============================================================================ + +TEST(Bodies, StarTargetAltitude) { + const auto& vega = VEGA; + StarTarget st(vega); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = st.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(VEGA)); + + for (const auto& t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} From ebbc59b9a31e4d896c4771e6fa6882dc4fb6554f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:48:25 +0100 Subject: [PATCH 3/6] Enhance documentation and examples for siderust-cpp - Updated mainpage.md to include new features: azimuth calculations, target tracking, and lunar phase events. - Refined README.md in examples directory to reflect new build instructions and added examples. - Expanded altitude_events_example.cpp to demonstrate altitude and azimuth calculations for multiple celestial bodies. - Introduced azimuth_lunar_phase_example.cpp to showcase azimuth events and lunar phase geometry. - Improved coordinate_systems_example.cpp with compile-time frame transformations and observer details. - Enhanced coordinates_examples.cpp with detailed typed-coordinate construction and conversion examples. - Revamped demo.cpp for a comprehensive end-to-end demonstration of siderust capabilities. - Updated solar_system_bodies_example.cpp to include body dispatch API and ephemeris calculations. - Added trackable_targets_example.cpp to illustrate polymorphic tracking of celestial targets. --- CMakeLists.txt | 18 +++ README.md | 35 ++++-- docs/mainpage.md | 29 +++-- examples/README.md | 28 +++-- examples/altitude_events_example.cpp | 100 +++++++++++---- examples/azimuth_lunar_phase_example.cpp | 125 +++++++++++++++++++ examples/coordinate_systems_example.cpp | 54 +++++++-- examples/coordinates_examples.cpp | 58 ++++++--- examples/demo.cpp | 147 +++++++++++++++++++---- examples/solar_system_bodies_example.cpp | 85 ++++++++++--- examples/trackable_targets_example.cpp | 81 +++++++++++++ 11 files changed, 627 insertions(+), 133 deletions(-) create mode 100644 examples/azimuth_lunar_phase_example.cpp create mode 100644 examples/trackable_targets_example.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 441a999..8131d86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,24 @@ if(DEFINED _siderust_rpath) ) endif() +add_executable(trackable_targets_example examples/trackable_targets_example.cpp) +target_link_libraries(trackable_targets_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(trackable_targets_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(azimuth_lunar_phase_example examples/azimuth_lunar_phase_example.cpp) +target_link_libraries(azimuth_lunar_phase_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(azimuth_lunar_phase_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- diff --git a/README.md b/README.md index 8791500..92d7497 100644 --- a/README.md +++ b/README.md @@ -10,38 +10,39 @@ Modern, header-only C++17 wrapper for **siderust** — a high-precision astronom |--------|-------------| | **Time** (`time.hpp`) | `JulianDate`, `MJD`, `UTC`, `Period` — value types with arithmetic and UTC round-trips | | **Coordinates** (`coordinates.hpp`) | Modular typed API (`coordinates/{geodetic,spherical,cartesian,types}.hpp`) plus selective alias headers under `coordinates/types/{spherical,cartesian}/...` | +| **Frames & Centers** (`frames.hpp`, `centers.hpp`) | Compile-time frame/center tags and transform capability traits | | **Bodies** (`bodies.hpp`) | `Star` (RAII, catalog + custom), `Planet` (8 planets), `ProperMotion`, `Orbit` | | **Observatories** (`observatories.hpp`) | Named sites: Roque de los Muchachos, Paranal, Mauna Kea, La Silla | | **Altitude** (`altitude.hpp`) | Sun / Moon / Star / ICRS altitude: instant, above/below threshold, crossings, culminations | +| **Azimuth** (`azimuth.hpp`) | Sun / Moon / Star / ICRS azimuth: instant, crossings, extrema, range windows | +| **Targets** (`trackable.hpp`, `target.hpp`, `body_target.hpp`, `star_target.hpp`) | Polymorphic tracking with `Trackable`, `Target`, `BodyTarget`, and `StarTarget` | +| **Lunar Phase** (`lunar_phase.hpp`) | Phase geometry/labels, principal phase events, illumination window search | | **Ephemeris** (`ephemeris.hpp`) | VSOP87 Sun/Earth positions, ELP2000 Moon position | ## Quick Start ```cpp #include -#include +#include int main() { using namespace siderust; - using namespace qtty::literals; auto obs = ROQUE_DE_LOS_MUCHACHOS; auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto mjd = MJD::from_jd(jd); + auto win = Period(mjd, mjd + qtty::Day(1.0)); - // Sun altitude - qtty::Radian alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad\n", alt.value()); + qtty::Degree sun_alt = sun::altitude_at(obs, mjd).to(); + qtty::Degree sun_az = sun::azimuth_at(obs, mjd); + std::cout << "Sun alt=" << sun_alt.value() << " deg" + << " az=" << sun_az.value() << " deg\n"; - // Star from catalog - const auto& vega = VEGA; - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad\n", star_alt.value()); + Target fixed(279.23473, 38.78369); // Vega-like ICRS target + std::cout << "Target alt=" << fixed.altitude_at(obs, mjd).value() << " deg\n"; - // Night periods (astronomical twilight) - auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); - for (auto& p : nights) - std::printf("Night: MJD %.4f – %.4f\n", p.start_mjd(), p.end_mjd()); + auto nights = sun::below_threshold(obs, win, qtty::Degree(-18.0)); + std::cout << "Astronomical-night periods in next 24h: " << nights.size() << "\n"; return 0; } @@ -65,6 +66,8 @@ cmake --build . ./coordinate_systems_example ./solar_system_bodies_example ./altitude_events_example +./trackable_targets_example +./azimuth_lunar_phase_example # Run tests ctest --output-on-failure @@ -153,6 +156,12 @@ siderust-cpp/ │ ├── bodies.hpp ← Star, Planet, ProperMotion │ ├── observatories.hpp ← named observatory locations │ ├── altitude.hpp ← sun/moon/star altitude API +│ ├── azimuth.hpp ← azimuth queries and events +│ ├── lunar_phase.hpp ← moon phase geometry and events +│ ├── trackable.hpp ← polymorphic trackable interface +│ ├── target.hpp ← fixed ICRS target (RAII) +│ ├── body_target.hpp ← body enum trackable adapter +│ ├── star_target.hpp ← star trackable adapter │ └── ephemeris.hpp ← VSOP87/ELP2000 positions ├── examples/demo.cpp ├── tests/ diff --git a/docs/mainpage.md b/docs/mainpage.md index f378652..d24001f 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -21,6 +21,9 @@ codebase without writing a single line of Rust. | **Bodies** (`bodies.hpp`) | `Star` (RAII, catalog + custom), `Planet` (8 planets), `ProperMotion`, `Orbit` | | **Observatories** (`observatories.hpp`) | Named sites: Roque de los Muchachos, Paranal, Mauna Kea, La Silla | | **Altitude** (`altitude.hpp`) | Sun / Moon / Star / ICRS altitude: instant, above/below threshold, crossings, culminations | +| **Azimuth** (`azimuth.hpp`) | Sun / Moon / Star / ICRS azimuth: instant, crossings, extrema, range windows | +| **Targets** (`trackable.hpp`, `target.hpp`, `body_target.hpp`, `star_target.hpp`) | Polymorphic target tracking across bodies, stars, and fixed ICRS directions | +| **Lunar Phase** (`lunar_phase.hpp`) | Moon phase geometry, labels, principal phase events, illumination windows | | **Ephemeris** (`ephemeris.hpp`) | VSOP87 Sun/Earth positions, ELP2000 Moon position | --- @@ -34,25 +37,22 @@ codebase without writing a single line of Rust. int main() { using namespace siderust; - using namespace qtty::literals; auto obs = ROQUE_DE_LOS_MUCHACHOS; auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto mjd = MJD::from_jd(jd); + auto win = Period(mjd, mjd + qtty::Day(1.0)); - // Sun altitude at the observatory - qtty::Radian alt = sun::altitude_at(obs, mjd); - std::cout << std::fixed << std::setprecision(4) << "Sun altitude: " << alt << " rad\n"; + qtty::Degree sun_alt = sun::altitude_at(obs, mjd).to(); + qtty::Degree sun_az = sun::azimuth_at(obs, mjd); + std::cout << "Sun alt=" << sun_alt.value() << " deg" + << " az=" << sun_az.value() << " deg\n"; - // Star from built-in catalog - const auto& vega = VEGA; - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::cout << "Vega altitude: " << star_alt << " rad\n"; + Target fixed(279.23473, 38.78369); // Vega-like fixed ICRS target + std::cout << "Target alt=" << fixed.altitude_at(obs, mjd).value() << " deg\n"; - // Astronomical night periods (twilight < -18°) - auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); - for (auto& p : nights) - std::cout << "Night: MJD " << p.start() << " – " << p.end() << "\n"; + auto nights = sun::below_threshold(obs, win, qtty::Degree(-18.0)); + std::cout << "Astronomical-night periods in next 24h: " << nights.size() << "\n"; return 0; } @@ -113,6 +113,8 @@ cmake --build . ./coordinate_systems_example ./solar_system_bodies_example ./altitude_events_example +./trackable_targets_example +./azimuth_lunar_phase_example # Run tests ctest --output-on-failure @@ -131,6 +133,9 @@ ctest --output-on-failure - `siderust/bodies.hpp` — `Star`, `Planet`, and orbital / proper-motion types - `siderust/observatories.hpp` — known observatory locations and custom geodetic points - `siderust/altitude.hpp` — Sun / Moon / Star altitude queries and event search +- `siderust/azimuth.hpp` — azimuth queries, crossings, extrema, and azimuth ranges +- `siderust/trackable.hpp`, `siderust/target.hpp`, `siderust/body_target.hpp`, `siderust/star_target.hpp` — target abstractions and polymorphic tracking +- `siderust/lunar_phase.hpp` — moon phase geometry, labels, phase events, illumination windows - `siderust/ephemeris.hpp` — VSOP87 / ELP2000 position queries --- diff --git a/examples/README.md b/examples/README.md index ac3ef29..1287282 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,24 +3,28 @@ Build from the repository root: ```bash -cmake -S . -B build-make -cmake --build build-make +cmake -S . -B build +cmake --build build ``` Run selected examples: ```bash -./build-make/demo -./build-make/coordinates_examples -./build-make/coordinate_systems_example -./build-make/solar_system_bodies_example -./build-make/altitude_events_example +./build/siderust_demo +./build/coordinates_examples +./build/coordinate_systems_example +./build/solar_system_bodies_example +./build/altitude_events_example +./build/trackable_targets_example +./build/azimuth_lunar_phase_example ``` ## Files -- `demo.cpp`: broad API walkthrough. -- `coordinates_examples.cpp`: typed coordinate creation and frame transforms. -- `coordinate_systems_example.cpp`: coordinate systems + direction/position transforms in one place. -- `solar_system_bodies_example.cpp`: ephemeris vectors and static planet catalog data. -- `altitude_events_example.cpp`: altitude periods, crossings, and culminations for Sun, Moon, VEGA, and fixed ICRS directions. +- `demo.cpp`: end-to-end extended walkthrough (time, typed coordinates, altitude/azimuth, trackables, ephemeris, lunar phase). +- `coordinates_examples.cpp`: typed coordinate construction and core conversion patterns. +- `coordinate_systems_example.cpp`: frame-tag traits and practical frame/horizontal transforms. +- `solar_system_bodies_example.cpp`: planet catalog constants, body-dispatch API, and ephemeris vectors. +- `altitude_events_example.cpp`: altitude windows/crossings/culminations for Sun, Moon, stars, ICRS directions, and `Target`. +- `trackable_targets_example.cpp`: polymorphic tracking with `Trackable`, `BodyTarget`, `StarTarget`, and `Target`. +- `azimuth_lunar_phase_example.cpp`: azimuth events/ranges plus lunar phase geometry, labels, and phase-event searches. diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 30d6adb..1a0249b 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -1,41 +1,99 @@ /** * @file altitude_events_example.cpp * @example altitude_events_example.cpp - * @brief Concise altitude events example using streamed UTC and Period printing. + * @brief Altitude periods/crossings/culminations for multiple target types. */ -#include -#include +#include #include +#include #include +#include + +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +const char* culmination_kind_name(siderust::CulminationKind kind) { + using siderust::CulminationKind; + switch (kind) { + case CulminationKind::Max: + return "max"; + case CulminationKind::Min: + return "min"; + } + return "unknown"; +} + +void print_periods(const std::vector& periods, std::size_t limit) { + const std::size_t n = std::min(periods.size(), limit); + for (std::size_t i = 0; i < n; ++i) { + const auto& p = periods[i]; + std::cout << " " << (i + 1) << ") " + << p.start().to_utc() << " -> " << p.end().to_utc() + << " (" << std::fixed << std::setprecision(2) + << p.duration().value() << " h)\n"; + } +} + +} // namespace + int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== altitude_events_example (concise) ===\n"; + const Geodetic obs = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(2.0); + const Period window(start, end); - const auto obs = MAUNA_KEA; - const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const auto end = start + qtty::Day(2.0); + SearchOptions opts; + opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step - auto nights = sun::below_threshold(obs, start, end, -18.0_deg); - std::cout << "Astronomical nights found: " << nights.size() << "\n"; + std::cout << "=== altitude_events_example ===\n"; + std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - // Print up to three night periods as UTC ranges and duration in hours - const std::size_t n = std::min(nights.size(), 3); - for (std::size_t i = 0; i < n; ++i) { - const auto &p = nights[i]; - std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " << p.end().to_utc() - << " (" << std::fixed << std::setprecision(2) << p.duration().value() << " h)\n"; + auto sun_nights = sun::below_threshold(obs, window, qtty::Degree(-18.0), opts); + std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; + print_periods(sun_nights, 3); + + auto sun_cross = sun::crossings(obs, window, qtty::Degree(0.0), opts); + std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; + if (!sun_cross.empty()) { + const auto& c = sun_cross.front(); + std::cout << " First crossing: " << c.time.to_utc() + << " (" << crossing_direction_name(c.direction) << ")\n"; } - auto crossings = sun::crossings(obs, start, end, 0.0_deg); - std::cout << "Sun crossings: " << crossings.size() << "\n"; + auto moon_culm = moon::culminations(obs, window, opts); + std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; + if (!moon_culm.empty()) { + const auto& c = moon_culm.front(); + std::cout << " First culmination: " << c.time.to_utc() + << " kind=" << culmination_kind_name(c.kind) + << " alt=" << c.altitude.value() << " deg\n"; + } + + auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, qtty::Degree(30.0), opts); + std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; + print_periods(vega_periods, 2); + + spherical::direction::ICRS target_dir(279.23473, 38.78369); + auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, qtty::Degree(0.0), opts); + std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - auto culminations = sun::culminations(obs, start, end); - std::cout << "Culminations: " << culminations.size() << "\n"; + Target fixed_target(279.23473, 38.78369); + auto fixed_target_periods = fixed_target.above_threshold(obs, window, qtty::Degree(45.0), opts); + std::cout << "Target::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp new file mode 100644 index 0000000..a90ad1f --- /dev/null +++ b/examples/azimuth_lunar_phase_example.cpp @@ -0,0 +1,125 @@ +/** + * @file azimuth_lunar_phase_example.cpp + * @example azimuth_lunar_phase_example.cpp + * @brief Azimuth event search plus lunar phase geometry/events. + */ + +#include +#include +#include +#include + +#include + +namespace { + +const char* az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; +} + +const char* phase_kind_name(siderust::PhaseKind kind) { + using siderust::PhaseKind; + switch (kind) { + case PhaseKind::NewMoon: + return "new moon"; + case PhaseKind::FirstQuarter: + return "first quarter"; + case PhaseKind::FullMoon: + return "full moon"; + case PhaseKind::LastQuarter: + return "last quarter"; + } + return "unknown"; +} + +const char* phase_label_name(siderust::MoonPhaseLabel label) { + using siderust::MoonPhaseLabel; + switch (label) { + case MoonPhaseLabel::NewMoon: + return "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return "full moon"; + case MoonPhaseLabel::WaningGibbous: + return "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return "waning crescent"; + } + return "unknown"; +} + +} // namespace + +int main() { + using namespace siderust; + + const Geodetic site = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(3.0); + const Period window(start, end); + + std::cout << "=== azimuth_lunar_phase_example ===\n"; + std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + + const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); + std::cout << "Instant azimuth\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now).value() << " deg\n"; + std::cout << " Moon : " << moon::azimuth_at(site, now).value() << " deg\n"; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now).value() << " deg\n\n"; + + auto sun_cross = sun::azimuth_crossings(site, window, qtty::Degree(180.0)); + auto sun_ext = sun::azimuth_extrema(site, window); + auto moon_west = moon::in_azimuth_range(site, window, qtty::Degree(240.0), qtty::Degree(300.0)); + + std::cout << "Azimuth events\n"; + std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; + std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; + if (!sun_ext.empty()) { + const auto& e = sun_ext.front(); + std::cout << " first extremum " << az_kind_name(e.kind) + << " at " << e.time.to_utc() + << " az=" << e.azimuth.value() << " deg\n"; + } + std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; + + const JulianDate jd_now = now.to_jd(); + auto geo_phase = moon::phase_geocentric(jd_now); + auto topo_phase = moon::phase_topocentric(jd_now, site); + auto topo_label = moon::phase_label(topo_phase); + + auto phase_events = moon::find_phase_events( + Period(start, start + qtty::Day(30.0))); + auto half_lit = moon::illumination_range(window, 0.45, 0.55); + + std::cout << "Lunar phase\n"; + std::cout << std::fixed << std::setprecision(3) + << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" + << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction + << " (" << phase_label_name(topo_label) << ")\n"; + + std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; + const std::size_t n = std::min(phase_events.size(), 4); + for (std::size_t i = 0; i < n; ++i) { + const auto& ev = phase_events[i]; + std::cout << " " << ev.time.to_utc() << " -> " << phase_kind_name(ev.kind) << "\n"; + } + + std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " + << half_lit.size() << "\n"; + + return 0; +} diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index 5cf8f9c..aac5108 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -1,28 +1,56 @@ /** * @file coordinate_systems_example.cpp * @example coordinate_systems_example.cpp - * @brief Coordinate systems and frame transform walkthrough. - * - * Usage: - * cmake --build build-make --target coordinate_systems_example - * ./build-make/coordinate_systems_example + * @brief Compile-time frame tags and transform capabilities walkthrough. */ -#include - -#include #include +#include +#include + +#include int main() { using namespace siderust; - using namespace qtty::literals; + using namespace siderust::frames; + + std::cout << "=== coordinate_systems_example ===\n"; + + static_assert(has_frame_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_horizontal_transform_v); + + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + + spherical::Direction src(279.23473, 38.78369); + const auto ecl = src.to_frame(jd); + const auto mod = src.to_frame(jd); + const auto tod = mod.to_frame(jd); + const auto horiz = src.to_horizontal(jd, observer); - std::cout << "=== coordinate_systems_example===\n"; + std::cout << std::fixed << std::setprecision(6); + std::cout << "Observer\n"; + std::cout << " lon=" << observer.lon.value() << " deg" + << " lat=" << observer.lat.value() << " deg\n\n"; - auto obs = ROQUE_DE_LOS_MUCHACHOS; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " + << src.ra().value() << ", " << src.dec().value() << " deg\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " + << ecl.lon().value() << ", " << ecl.lat().value() << " deg\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " + << mod.ra().value() << ", " << mod.dec().value() << " deg\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " + << tod.ra().value() << ", " << tod.dec().value() << " deg\n"; + std::cout << " Horizontal az/alt : " + << horiz.az().value() << ", " << horiz.alt().value() << " deg\n\n"; - std::cout << "Observer lon=" << std::fixed << std::setprecision(4) << obs.lon - << " lat=" << obs.lat << "\n"; + const auto ecef = observer.to_cartesian(); + std::cout << "Observer in ECEF\n"; + std::cout << " x=" << ecef.x().value() << " km" + << " y=" << ecef.y().value() << " km" + << " z=" << ecef.z().value() << " km\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index 0a2ecf5..16b2b7d 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -1,35 +1,57 @@ /** * @file coordinates_examples.cpp * @example coordinates_examples.cpp - * @brief Focused examples for creating and converting typed coordinates. + * @brief Focused typed-coordinate construction and conversion examples. */ -#include -#include #include +#include +#include + +#include int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== coordinates_examples (concise) ===\n"; + std::cout << "=== coordinates_examples ===\n"; + + const Geodetic site(-17.8890, 28.7610, 2396.0); + const auto ecef_m = site.to_cartesian(); + const auto ecef_km = site.to_cartesian(); - // Geodetic -> ECEF (single line) - Geodetic obs(-17.8890, 28.7610, 2396.0); - auto ecef = obs.to_cartesian(); - std::cout << "Geodetic lon=" << std::fixed << std::setprecision(4) << obs.lon.value() - << " lat=" << obs.lat.value() << " h=" << obs.height.value() << " m\n"; + static_assert(std::is_same_v< + std::remove_cv_t, + cartesian::position::ECEF>); + + std::cout << "Geodetic -> ECEF\n"; + std::cout << " lon=" << site.lon.value() << " deg lat=" << site.lat.value() + << " deg h=" << site.height.value() << " m\n"; + std::cout << " x=" << std::fixed << std::setprecision(3) + << ecef_m.x().value() << " m" + << " (" << ecef_km.x().value() << " km)\n\n"; + + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - // Spherical direction example (ICRS -> horizontal) spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto hor = vega_icrs.to_horizontal(jd, obs); - std::cout << "Vega az=" << std::setprecision(2) << hor.az().value() << " alt=" << hor.alt().value() << "\n"; + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_true = vega_icrs.to_frame(jd); + auto vega_horiz = vega_icrs.to_horizontal(jd, site); + + std::cout << "Direction transforms\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs.ra().value() << ", " << vega_icrs.dec().value() << " deg\n"; + std::cout << " Ecliptic lon/lat: " << vega_ecl.lon().value() << ", " << vega_ecl.lat().value() << " deg\n"; + std::cout << " True-of-date RA/Dec: " << vega_true.ra().value() << ", " << vega_true.dec().value() << " deg\n"; + std::cout << " Horizontal az/alt: " << vega_horiz.az().value() << ", " << vega_horiz.alt().value() << " deg\n\n"; + + spherical::position::ICRS synthetic_star( + qtty::Degree(210.0), qtty::Degree(-12.0), qtty::AstronomicalUnit(4.2)); + + cartesian::position::EclipticMeanJ2000 earth = + ephemeris::earth_heliocentric(jd); - // Ephemeris quick values - auto earth = ephemeris::earth_heliocentric(jd); - std::cout << "Earth x=" << std::setprecision(6) << earth.x().value() << " AU\n"; + std::cout << "Typed positions\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance().value() << " AU\n"; + std::cout << " Earth heliocentric x: " << earth.x().value() << " AU\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 578bc6b..ee47365 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -1,45 +1,142 @@ /** * @file demo.cpp * @example demo.cpp - * @brief Demonstrates the siderust C++ API. - * - * Usage: - * cd build && cmake .. && cmake --build . && ./demo + * @brief End-to-end demo of siderust-cpp extended capabilities. */ +#include #include -#include #include +#include +#include +#include + #include +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +const char* moon_phase_label_name(siderust::MoonPhaseLabel label) { + using siderust::MoonPhaseLabel; + switch (label) { + case MoonPhaseLabel::NewMoon: + return "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return "full moon"; + case MoonPhaseLabel::WaningGibbous: + return "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return "waning crescent"; + } + return "unknown"; +} + +} // namespace + int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== siderust-cpp demo (concise) ===\n"; + const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period next_day(now, now + qtty::Day(1.0)); + + std::cout << "=== siderust-cpp extended demo ===\n"; + std::cout << "Observer: lon=" << site.lon.value() + << " deg lat=" << site.lat.value() + << " deg h=" << site.height.value() << " m\n"; + std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() + << " UTC " << jd.to_utc() << "\n\n"; + + spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_hor = vega_icrs.to_horizontal(jd, site); + std::cout << "Typed coordinates\n"; + std::cout << " Vega ICRS RA=" << vega_icrs.ra().value() + << " deg Dec=" << vega_icrs.dec().value() << " deg\n"; + std::cout << " Vega Ecliptic lon=" << vega_ecl.lon().value() + << " deg lat=" << vega_ecl.lat().value() << " deg\n"; + std::cout << " Vega Horizontal az=" << vega_hor.az().value() + << " deg alt=" << vega_hor.alt().value() << " deg\n\n"; + + qtty::Degree sun_alt = sun::altitude_at(site, now).to(); + qtty::Degree sun_az = sun::azimuth_at(site, now); + std::cout << "Sun instant\n"; + std::cout << " Altitude=" << sun_alt.value() << " deg" + << " Azimuth=" << sun_az.value() << " deg\n"; + + auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); + if (!sun_crossings.empty()) { + std::cout << " Next horizon crossing: " + << sun_crossings.front().time.to_utc() << " (" + << crossing_direction_name(sun_crossings.front().direction) + << ")\n"; + } + std::cout << "\n"; + + BodyTarget mars(Body::Mars); + Target fixed_target(279.23473, 38.78369); // Vega-like fixed ICRS pointing + + std::vector>> targets; + targets.push_back({"Sun", std::make_unique(Body::Sun)}); + targets.push_back({"Vega", std::make_unique(VEGA)}); + targets.push_back({"Fixed target", std::make_unique(279.23473, 38.78369)}); - // Time - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - std::cout << "JD=" << std::fixed << std::setprecision(6) << jd.value() << " UTC=" << jd.to_utc() << "\n"; + std::cout << "Trackable polymorphism\n"; + for (const auto& entry : targets) { + const auto& name = entry.first; + const auto& obj = entry.second; + auto alt = obj->altitude_at(site, now); + auto az = obj->azimuth_at(site, now); + std::cout << " " << std::setw(12) << std::left << name + << " alt=" << std::setw(8) << alt.value() + << " deg az=" << az.value() << " deg\n"; + } + std::cout << " Mars altitude via BodyTarget: " + << mars.altitude_at(site, now).value() << " deg\n"; + std::cout << " Fixed Target altitude: " + << fixed_target.altitude_at(site, now).value() << " deg\n\n"; - // Sun altitude (deg) - auto mjd = MJD::from_jd(jd); - qtty::Radian sun_alt = sun::altitude_at(ROQUE_DE_LOS_MUCHACHOS, mjd); - std::cout << "Sun alt=" << std::fixed << std::setprecision(2) << sun_alt.to().value() << " deg\n"; + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt( + moon_geo.x().value() * moon_geo.x().value() + + moon_geo.y().value() * moon_geo.y().value() + + moon_geo.z().value() * moon_geo.z().value()); - // Vega altitude - const auto& vega = VEGA; - auto vega_alt = star_altitude::altitude_at(vega, ROQUE_DE_LOS_MUCHACHOS, mjd); - std::cout << "Vega alt=" << std::setprecision(2) << vega_alt.to().value() << " deg\n"; + std::cout << "Ephemeris\n"; + std::cout << " Earth heliocentric x=" << earth_helio.x().value() + << " AU y=" << earth_helio.y().value() << " AU\n"; + std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - // Simple ephemeris values - auto earth = ephemeris::earth_heliocentric(jd); - std::cout << "Earth (AU) x=" << std::setprecision(6) << earth.x().value() << " y=" << earth.y().value() << "\n"; + auto phase = moon::phase_topocentric(jd, site); + auto label = moon::phase_label(phase); + auto bright_periods = moon::illumination_above( + Period(now, now + qtty::Day(7.0)), 0.8); - auto moon = ephemeris::moon_geocentric(jd); - double moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); - std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_r << " km\n"; + std::cout << "Lunar phase\n"; + std::cout << " Illuminated fraction=" << phase.illuminated_fraction + << " label=" << moon_phase_label_name(label) << "\n"; + std::cout << " Bright-moon periods (next 7 days, k>=0.8): " + << bright_periods.size() << "\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index d34a7ea..0159538 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -1,36 +1,83 @@ /** * @file solar_system_bodies_example.cpp * @example solar_system_bodies_example.cpp - * @brief Solar-system body ephemeris and catalog examples. - * - * Usage: - * cmake --build build-make --target solar_system_bodies_example - * ./build-make/solar_system_bodies_example + * @brief Solar-system body catalog, ephemeris, and body-dispatch examples. */ -#include -#include -#include #include +#include +#include +#include + +#include + +namespace { + +const char* az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; +} + +} // namespace int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== solar_system_bodies_example (concise) ===\n"; + const Geodetic site = MAUNA_KEA; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period window(now, now + qtty::Day(2.0)); + + std::cout << "=== solar_system_bodies_example ===\n"; + std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; + + std::cout << "Planet catalog constants\n"; + std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" + << " radius=" << MERCURY.radius_km << " km\n"; + std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" + << " radius=" << EARTH.radius_km << " km\n"; + std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" + << " radius=" << JUPITER.radius_km << " km\n\n"; + + auto earth = ephemeris::earth_heliocentric(jd); + auto moon_pos = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt( + moon_pos.x().value() * moon_pos.x().value() + + moon_pos.y().value() * moon_pos.y().value() + + moon_pos.z().value() * moon_pos.z().value()); - auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::cout << "Epoch JD=" << std::fixed << std::setprecision(6) << jd.value() << "\n"; + std::cout << "Ephemeris\n"; + std::cout << std::fixed << std::setprecision(6) + << " Earth heliocentric x=" << earth.x().value() + << " AU y=" << earth.y().value() << " AU\n"; + std::cout << std::setprecision(2) + << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - auto earth_helio = ephemeris::earth_heliocentric(jd); - std::cout << "Earth heliocentric x=" << std::setprecision(6) << earth_helio.x().value() << " AU\n"; + std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, Body::Jupiter}; - auto moon_geo = ephemeris::moon_geocentric(jd); - double moon_dist = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + moon_geo.y().value() * moon_geo.y().value() + moon_geo.z().value() * moon_geo.z().value()); - std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_dist << " km\n"; + std::cout << "Body dispatch API at observer\n"; + for (Body b : tracked) { + auto alt = body::altitude_at(b, site, now).to(); + auto az = body::azimuth_at(b, site, now).to(); + std::cout << " body=" << static_cast(b) + << " alt=" << alt.value() << " deg" + << " az=" << az.value() << " deg\n"; + } - // Print a couple of planets concisely - std::cout << "Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU Earth a=" << EARTH.orbit.semi_major_axis_au << " AU\n"; + auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); + if (!moon_extrema.empty()) { + const auto& e = moon_extrema.front(); + std::cout << "\nMoon azimuth extrema\n"; + std::cout << " first " << az_kind_name(e.kind) + << " at " << e.time.to_utc() + << " az=" << e.azimuth.value() << " deg\n"; + } return 0; } diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp new file mode 100644 index 0000000..a627b56 --- /dev/null +++ b/examples/trackable_targets_example.cpp @@ -0,0 +1,81 @@ +/** + * @file trackable_targets_example.cpp + * @example trackable_targets_example.cpp + * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + */ + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +struct NamedTrackable { + std::string name; + std::unique_ptr object; +}; + +} // namespace + +int main() { + using namespace siderust; + + const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); + const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); + const Period window(now, now + qtty::Day(1.0)); + + std::cout << "=== trackable_targets_example ===\n"; + std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; + + Target fixed_vega_like(279.23473, 38.78369); + std::cout << "Target metadata\n"; + std::cout << " RA=" << fixed_vega_like.ra_deg() << " deg" + << " Dec=" << fixed_vega_like.dec_deg() << " deg" + << " epoch JD=" << fixed_vega_like.epoch_jd() << "\n\n"; + + std::vector catalog; + catalog.push_back({"Sun", std::make_unique(Body::Sun)}); + catalog.push_back({"Mars", std::make_unique(Body::Mars)}); + catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); + catalog.push_back({"Fixed Vega-like Target", std::make_unique(279.23473, 38.78369)}); + + for (const auto& entry : catalog) { + const auto& t = entry.object; + auto alt = t->altitude_at(site, now); + auto az = t->azimuth_at(site, now); + + std::cout << std::left << std::setw(22) << entry.name << std::right + << " alt=" << std::setw(9) << alt.value() << " deg" + << " az=" << az.value() << " deg\n"; + + auto crossings = t->crossings(site, window, qtty::Degree(0.0)); + if (!crossings.empty()) { + const auto& first = crossings.front(); + std::cout << " first horizon crossing: " << first.time.to_utc() + << " (" << crossing_direction_name(first.direction) << ")\n"; + } + + auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); + if (!az_cross.empty()) { + std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() << "\n"; + } + } + + return 0; +} From 2617f9c28e45ece62e5b38cc5435f395e408239f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:52:15 +0100 Subject: [PATCH 4/6] refactor: streamline output for crossing direction and culmination kind in altitude events example --- examples/altitude_events_example.cpp | 26 ++------------------------ include/siderust/ffi_core.hpp | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 1a0249b..2884909 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -13,28 +13,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - -const char* culmination_kind_name(siderust::CulminationKind kind) { - using siderust::CulminationKind; - switch (kind) { - case CulminationKind::Max: - return "max"; - case CulminationKind::Min: - return "min"; - } - return "unknown"; -} - void print_periods(const std::vector& periods, std::size_t limit) { const std::size_t n = std::min(periods.size(), limit); for (std::size_t i = 0; i < n; ++i) { @@ -71,7 +49,7 @@ int main() { if (!sun_cross.empty()) { const auto& c = sun_cross.front(); std::cout << " First crossing: " << c.time.to_utc() - << " (" << crossing_direction_name(c.direction) << ")\n"; + << " (" << c.direction << ")\n"; } auto moon_culm = moon::culminations(obs, window, opts); @@ -79,7 +57,7 @@ int main() { if (!moon_culm.empty()) { const auto& c = moon_culm.front(); std::cout << " First culmination: " << c.time.to_utc() - << " kind=" << culmination_kind_name(c.kind) + << " kind=" << c.kind << " alt=" << c.altitude.value() << " deg\n"; } diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index d673029..71784ce 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -9,6 +9,7 @@ */ #include +#include #include #include @@ -163,6 +164,30 @@ enum class CulminationKind : int32_t { Min = SIDERUST_CULMINATION_KIND_T_MIN, }; +// ============================================================================ +// Stream operators for enums +// ============================================================================ + +inline std::ostream& operator<<(std::ostream& os, CrossingDirection dir) { + switch (dir) { + case CrossingDirection::Rising: + return os << "rising"; + case CrossingDirection::Setting: + return os << "setting"; + } + return os << "unknown"; +} + +inline std::ostream& operator<<(std::ostream& os, CulminationKind kind) { + switch (kind) { + case CulminationKind::Max: + return os << "max"; + case CulminationKind::Min: + return os << "min"; + } + return os << "unknown"; +} + enum class RaConvention : int32_t { MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, From 346ca20d93bd50b8109777ab07c82b90c7c998a5 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 01:31:35 +0100 Subject: [PATCH 5/6] Refactor target handling and streamline output formatting - Removed redundant functions for crossing direction and moon phase label names, replacing them with direct usage of enum values. - Updated output formatting in demo and example files to utilize operator overloads for cleaner code. - Introduced stream operators for various classes including AzimuthExtremumKind, Position, Geodetic, Direction, and lunar phase enums for improved output readability. - Enhanced Target class to support strongly-typed celestial directions, ensuring automatic conversion to ICRS where necessary. - Added tests for new Target functionality, verifying altitude calculations and typed accessors for ICRSTarget and EclipticMeanJ2000Target. - Updated coordinate tests to use qtty::Degree for consistency and clarity in direction initialization. --- examples/altitude_events_example.cpp | 20 +- examples/azimuth_lunar_phase_example.cpp | 70 +------ examples/coordinate_systems_example.cpp | 31 +-- examples/coordinates_examples.cpp | 29 ++- examples/demo.cpp | 66 ++---- examples/solar_system_bodies_example.cpp | 6 +- examples/trackable_targets_example.cpp | 50 +++-- include/siderust/azimuth.hpp | 18 ++ include/siderust/coordinates/cartesian.hpp | 14 ++ include/siderust/coordinates/geodetic.hpp | 13 ++ include/siderust/coordinates/spherical.hpp | 45 ++-- include/siderust/lunar_phase.hpp | 47 +++++ include/siderust/siderust.hpp | 2 +- include/siderust/target.hpp | 233 +++++++++++++++------ tests/test_altitude.cpp | 70 ++++++- tests/test_coordinates.cpp | 16 +- 16 files changed, 468 insertions(+), 262 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 2884909..ee88acf 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -28,6 +28,7 @@ void print_periods(const std::vector& periods, std::size_t lim int main() { using namespace siderust; + using namespace qtty::literals; const Geodetic obs = MAUNA_KEA; const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); @@ -40,11 +41,11 @@ int main() { std::cout << "=== altitude_events_example ===\n"; std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - auto sun_nights = sun::below_threshold(obs, window, qtty::Degree(-18.0), opts); + auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; print_periods(sun_nights, 3); - auto sun_cross = sun::crossings(obs, window, qtty::Degree(0.0), opts); + auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; if (!sun_cross.empty()) { const auto& c = sun_cross.front(); @@ -58,20 +59,21 @@ int main() { const auto& c = moon_culm.front(); std::cout << " First culmination: " << c.time.to_utc() << " kind=" << c.kind - << " alt=" << c.altitude.value() << " deg\n"; + << " alt=" << c.altitude + << std::endl; } - auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, qtty::Degree(30.0), opts); + auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; print_periods(vega_periods, 2); - spherical::direction::ICRS target_dir(279.23473, 38.78369); - auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, qtty::Degree(0.0), opts); + spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); + auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - Target fixed_target(279.23473, 38.78369); - auto fixed_target_periods = fixed_target.above_threshold(obs, window, qtty::Degree(45.0), opts); - std::cout << "Target::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; + ICRSTarget fixed_target{ spherical::direction::ICRS{279.23473_deg, 38.78369_deg } }; + auto fixed_target_periods = fixed_target.above_threshold(obs, window, 45.0_deg, opts); + std::cout << "ICRSTarget::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp index a90ad1f..f07d006 100644 --- a/examples/azimuth_lunar_phase_example.cpp +++ b/examples/azimuth_lunar_phase_example.cpp @@ -12,60 +12,11 @@ #include namespace { - -const char* az_kind_name(siderust::AzimuthExtremumKind kind) { - using siderust::AzimuthExtremumKind; - switch (kind) { - case AzimuthExtremumKind::Max: - return "max"; - case AzimuthExtremumKind::Min: - return "min"; - } - return "unknown"; -} - -const char* phase_kind_name(siderust::PhaseKind kind) { - using siderust::PhaseKind; - switch (kind) { - case PhaseKind::NewMoon: - return "new moon"; - case PhaseKind::FirstQuarter: - return "first quarter"; - case PhaseKind::FullMoon: - return "full moon"; - case PhaseKind::LastQuarter: - return "last quarter"; - } - return "unknown"; -} - -const char* phase_label_name(siderust::MoonPhaseLabel label) { - using siderust::MoonPhaseLabel; - switch (label) { - case MoonPhaseLabel::NewMoon: - return "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return "full moon"; - case MoonPhaseLabel::WaningGibbous: - return "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return "waning crescent"; - } - return "unknown"; -} - } // namespace int main() { using namespace siderust; + using namespace qtty::literals; const Geodetic site = MAUNA_KEA; const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); @@ -77,22 +28,23 @@ int main() { const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); std::cout << "Instant azimuth\n"; - std::cout << " Sun : " << sun::azimuth_at(site, now).value() << " deg\n"; - std::cout << " Moon : " << moon::azimuth_at(site, now).value() << " deg\n"; - std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now).value() << " deg\n\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; + std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) << std::endl; - auto sun_cross = sun::azimuth_crossings(site, window, qtty::Degree(180.0)); + auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); auto sun_ext = sun::azimuth_extrema(site, window); - auto moon_west = moon::in_azimuth_range(site, window, qtty::Degree(240.0), qtty::Degree(300.0)); + auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); std::cout << "Azimuth events\n"; std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; if (!sun_ext.empty()) { const auto& e = sun_ext.front(); - std::cout << " first extremum " << az_kind_name(e.kind) + std::cout << " first extremum " << e.kind << " at " << e.time.to_utc() - << " az=" << e.azimuth.value() << " deg\n"; + << " az=" << e.azimuth + << std::endl; } std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; @@ -109,13 +61,13 @@ int main() { std::cout << std::fixed << std::setprecision(3) << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction - << " (" << phase_label_name(topo_label) << ")\n"; + << " (" << topo_label << ")\n"; std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; const std::size_t n = std::min(phase_events.size(), 4); for (std::size_t i = 0; i < n; ++i) { const auto& ev = phase_events[i]; - std::cout << " " << ev.time.to_utc() << " -> " << phase_kind_name(ev.kind) << "\n"; + std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; } std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index aac5108..fcefb5a 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -21,36 +21,27 @@ int main() { static_assert(has_horizontal_transform_v); const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const auto ecef = observer.to_cartesian(); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::Direction src(279.23473, 38.78369); + spherical::Direction src(qtty::Degree(279.23473), qtty::Degree(38.78369)); const auto ecl = src.to_frame(jd); const auto mod = src.to_frame(jd); const auto tod = mod.to_frame(jd); const auto horiz = src.to_horizontal(jd, observer); std::cout << std::fixed << std::setprecision(6); - std::cout << "Observer\n"; - std::cout << " lon=" << observer.lon.value() << " deg" - << " lat=" << observer.lat.value() << " deg\n\n"; + std::cout << "Observer: " << observer << std::endl; + std::cout << "Observer in ECEF: " << ecef << std::endl; - std::cout << "Frame transforms for Vega-like direction\n"; - std::cout << " ICRS RA/Dec : " - << src.ra().value() << ", " << src.dec().value() << " deg\n"; - std::cout << " EclipticMeanJ2000 lon/lat : " - << ecl.lon().value() << ", " << ecl.lat().value() << " deg\n"; - std::cout << " EquatorialMeanOfDate RA/Dec: " - << mod.ra().value() << ", " << mod.dec().value() << " deg\n"; - std::cout << " EquatorialTrueOfDate RA/Dec: " - << tod.ra().value() << ", " << tod.dec().value() << " deg\n"; - std::cout << " Horizontal az/alt : " - << horiz.az().value() << ", " << horiz.alt().value() << " deg\n\n"; - const auto ecef = observer.to_cartesian(); - std::cout << "Observer in ECEF\n"; - std::cout << " x=" << ecef.x().value() << " km" - << " y=" << ecef.y().value() << " km" - << " z=" << ecef.z().value() << " km\n"; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " << src << "\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; + std::cout << " Horizontal az/alt : " << horiz << "\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index 16b2b7d..d13ccff 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -12,10 +12,11 @@ int main() { using namespace siderust; + using namespace qtty::literals; std::cout << "=== coordinates_examples ===\n"; - const Geodetic site(-17.8890, 28.7610, 2396.0); + const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); const auto ecef_m = site.to_cartesian(); const auto ecef_km = site.to_cartesian(); @@ -23,35 +24,33 @@ int main() { std::remove_cv_t, cartesian::position::ECEF>); - std::cout << "Geodetic -> ECEF\n"; - std::cout << " lon=" << site.lon.value() << " deg lat=" << site.lat.value() - << " deg h=" << site.height.value() << " m\n"; - std::cout << " x=" << std::fixed << std::setprecision(3) - << ecef_m.x().value() << " m" - << " (" << ecef_km.x().value() << " km)\n\n"; + std::cout << "Geodetic -> ECEF \n " + << site << "\n" + << ecef_m << "\n" + << "(" << ecef_km << ")\n"<< std::endl; const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); auto vega_ecl = vega_icrs.to_frame(jd); auto vega_true = vega_icrs.to_frame(jd); auto vega_horiz = vega_icrs.to_horizontal(jd, site); std::cout << "Direction transforms\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs.ra().value() << ", " << vega_icrs.dec().value() << " deg\n"; - std::cout << " Ecliptic lon/lat: " << vega_ecl.lon().value() << ", " << vega_ecl.lat().value() << " deg\n"; - std::cout << " True-of-date RA/Dec: " << vega_true.ra().value() << ", " << vega_true.dec().value() << " deg\n"; - std::cout << " Horizontal az/alt: " << vega_horiz.az().value() << ", " << vega_horiz.alt().value() << " deg\n\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; + std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; + std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; + std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; spherical::position::ICRS synthetic_star( - qtty::Degree(210.0), qtty::Degree(-12.0), qtty::AstronomicalUnit(4.2)); + 210.0_deg, -12.0_deg, 4.2_au); cartesian::position::EclipticMeanJ2000 earth = ephemeris::earth_heliocentric(jd); std::cout << "Typed positions\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance().value() << " AU\n"; - std::cout << " Earth heliocentric x: " << earth.x().value() << " AU\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance() << std::endl; + std::cout << " Earth heliocentric x: " << earth.x() << std::endl; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index ee47365..5ea5d96 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -15,40 +15,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - -const char* moon_phase_label_name(siderust::MoonPhaseLabel label) { - using siderust::MoonPhaseLabel; - switch (label) { - case MoonPhaseLabel::NewMoon: - return "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return "full moon"; - case MoonPhaseLabel::WaningGibbous: - return "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return "waning crescent"; - } - return "unknown"; -} - } // namespace int main() { @@ -60,22 +26,17 @@ int main() { const Period next_day(now, now + qtty::Day(1.0)); std::cout << "=== siderust-cpp extended demo ===\n"; - std::cout << "Observer: lon=" << site.lon.value() - << " deg lat=" << site.lat.value() - << " deg h=" << site.height.value() << " m\n"; + std::cout << "Observer: " << site << "\n"; std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() << " UTC " << jd.to_utc() << "\n\n"; - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto vega_ecl = vega_icrs.to_frame(jd); auto vega_hor = vega_icrs.to_horizontal(jd, site); std::cout << "Typed coordinates\n"; - std::cout << " Vega ICRS RA=" << vega_icrs.ra().value() - << " deg Dec=" << vega_icrs.dec().value() << " deg\n"; - std::cout << " Vega Ecliptic lon=" << vega_ecl.lon().value() - << " deg lat=" << vega_ecl.lat().value() << " deg\n"; - std::cout << " Vega Horizontal az=" << vega_hor.az().value() - << " deg alt=" << vega_hor.alt().value() << " deg\n\n"; + std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; + std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; + std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; qtty::Degree sun_alt = sun::altitude_at(site, now).to(); qtty::Degree sun_az = sun::azimuth_at(site, now); @@ -87,18 +48,20 @@ int main() { if (!sun_crossings.empty()) { std::cout << " Next horizon crossing: " << sun_crossings.front().time.to_utc() << " (" - << crossing_direction_name(sun_crossings.front().direction) + << sun_crossings.front().direction << ")\n"; } std::cout << "\n"; BodyTarget mars(Body::Mars); - Target fixed_target(279.23473, 38.78369); // Vega-like fixed ICRS pointing + ICRSTarget fixed_target{ spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369) } }; // Vega-like std::vector>> targets; targets.push_back({"Sun", std::make_unique(Body::Sun)}); targets.push_back({"Vega", std::make_unique(VEGA)}); - targets.push_back({"Fixed target", std::make_unique(279.23473, 38.78369)}); + targets.push_back({"Fixed target", std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); std::cout << "Trackable polymorphism\n"; for (const auto& entry : targets) { @@ -107,8 +70,8 @@ int main() { auto alt = obj->altitude_at(site, now); auto az = obj->azimuth_at(site, now); std::cout << " " << std::setw(12) << std::left << name - << " alt=" << std::setw(8) << alt.value() - << " deg az=" << az.value() << " deg\n"; + << " alt=" << std::setw(8) << alt + << " az=" << az << std::endl; } std::cout << " Mars altitude via BodyTarget: " << mars.altitude_at(site, now).value() << " deg\n"; @@ -123,8 +86,7 @@ int main() { moon_geo.z().value() * moon_geo.z().value()); std::cout << "Ephemeris\n"; - std::cout << " Earth heliocentric x=" << earth_helio.x().value() - << " AU y=" << earth_helio.y().value() << " AU\n"; + std::cout << " Earth heliocentric " << earth_helio << " AU\n"; std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; auto phase = moon::phase_topocentric(jd, site); @@ -134,7 +96,7 @@ int main() { std::cout << "Lunar phase\n"; std::cout << " Illuminated fraction=" << phase.illuminated_fraction - << " label=" << moon_phase_label_name(label) << "\n"; + << " label=" << label << "\n"; std::cout << " Bright-moon periods (next 7 days, k>=0.8): " << bright_periods.size() << "\n"; diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index 0159538..51971eb 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -66,8 +66,8 @@ int main() { auto alt = body::altitude_at(b, site, now).to(); auto az = body::azimuth_at(b, site, now).to(); std::cout << " body=" << static_cast(b) - << " alt=" << alt.value() << " deg" - << " az=" << az.value() << " deg\n"; + << " alt=" << alt + << " az=" << az << std::endl; } auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); @@ -76,7 +76,7 @@ int main() { std::cout << "\nMoon azimuth extrema\n"; std::cout << " first " << az_kind_name(e.kind) << " at " << e.time.to_utc() - << " az=" << e.azimuth.value() << " deg\n"; + << " az=" << e.azimuth << std::endl; } return 0; diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp index a627b56..e8e33b3 100644 --- a/examples/trackable_targets_example.cpp +++ b/examples/trackable_targets_example.cpp @@ -1,7 +1,14 @@ /** * @file trackable_targets_example.cpp * @example trackable_targets_example.cpp - * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * + * Demonstrates the strongly-typed Target template with multiple frames: + * - ICRSTarget — fixed direction in ICRS equatorial coordinates + * - EclipticMeanJ2000Target — fixed direction in mean ecliptic J2000 + * + * Non-ICRS targets are silently converted to ICRS at construction time for + * the Rust FFI layer; the original typed direction is retained in C++. */ #include @@ -15,17 +22,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - struct NamedTrackable { std::string name; std::unique_ptr object; @@ -43,17 +39,29 @@ int main() { std::cout << "=== trackable_targets_example ===\n"; std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; - Target fixed_vega_like(279.23473, 38.78369); - std::cout << "Target metadata\n"; - std::cout << " RA=" << fixed_vega_like.ra_deg() << " deg" - << " Dec=" << fixed_vega_like.dec_deg() << " deg" - << " epoch JD=" << fixed_vega_like.epoch_jd() << "\n\n"; + // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. + ICRSTarget fixed_vega_like{ spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369) } }; + std::cout << "ICRSTarget metadata\n"; + std::cout << " RA/Dec=" << fixed_vega_like.direction() + << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; + + // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). + // Automatically converted to ICRS by the constructor. + EclipticMeanJ2000Target ecliptic_vega{ spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8) } }; + auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); + std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; + std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; + std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() << " (converted)\n"; + std::cout << " alt=" << alt_ecliptic << std::endl; std::vector catalog; catalog.push_back({"Sun", std::make_unique(Body::Sun)}); catalog.push_back({"Mars", std::make_unique(Body::Mars)}); catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); - catalog.push_back({"Fixed Vega-like Target", std::make_unique(279.23473, 38.78369)}); + catalog.push_back({"Fixed Vega-like (ICRS)", std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); for (const auto& entry : catalog) { const auto& t = entry.object; @@ -61,14 +69,14 @@ int main() { auto az = t->azimuth_at(site, now); std::cout << std::left << std::setw(22) << entry.name << std::right - << " alt=" << std::setw(9) << alt.value() << " deg" - << " az=" << az.value() << " deg\n"; + << " alt=" << std::setw(9) << alt + << " az=" << az << std::endl; auto crossings = t->crossings(site, window, qtty::Degree(0.0)); if (!crossings.empty()) { const auto& first = crossings.front(); std::cout << " first horizon crossing: " << first.time.to_utc() - << " (" << crossing_direction_name(first.direction) << ")\n"; + << " (" << first.direction << ")\n"; } auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 7f1d4f7..4f88f40 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -21,6 +21,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include #include namespace siderust { @@ -386,4 +387,21 @@ inline std::vector azimuth_crossings( } // namespace icrs_altitude +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for AzimuthExtremumKind. + */ +inline std::ostream& operator<<(std::ostream& os, AzimuthExtremumKind kind) { + switch (kind) { + case AzimuthExtremumKind::Max: + return os << "max"; + case AzimuthExtremumKind::Min: + return os << "min"; + } + return os << "unknown"; +} + } // namespace siderust diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 79a069b..13adad1 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -12,6 +12,8 @@ #include +#include + namespace siderust { namespace cartesian { @@ -86,5 +88,17 @@ struct Position { } }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Position. + */ +template +inline std::ostream& operator<<(std::ostream& os, const Position& pos) { + return os << pos.x() << ", " << pos.y() << ", " << pos.z(); +} + } // namespace cartesian } // namespace siderust diff --git a/include/siderust/coordinates/geodetic.hpp b/include/siderust/coordinates/geodetic.hpp index 86dc552..d244b13 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -12,6 +12,8 @@ #include +#include + namespace siderust { namespace cartesian { template @@ -60,4 +62,15 @@ struct Geodetic { cartesian::Position to_cartesian() const; }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Geodetic. + */ +inline std::ostream& operator<<(std::ostream& os, const Geodetic& geo) { + return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; +} + } // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 58f551e..56db30d 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -13,6 +13,7 @@ #include +#include #include namespace siderust { @@ -24,7 +25,8 @@ namespace spherical { * Mirrors Rust's `affn::spherical::Direction`. * * @ingroup coordinates_spherical - * @tparam F Reference frame tag (e.g. `frames::ICRS`). + * @tparam F Reference frame chapter content removed. Restore the original from \texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. +tag (e.g. `frames::ICRS`). * * @par Accessors * Access values through frame-appropriate getters: @@ -46,10 +48,6 @@ struct Direction { Direction(qtty::Degree azimuth, qtty::Degree polar) : azimuth_(azimuth), polar_(polar) {} - /// Raw-double convenience (degrees). - Direction(double azimuth_deg, double polar_deg) - : azimuth_(qtty::Degree(azimuth_deg)), polar_(qtty::Degree(polar_deg)) {} - /// @name Frame info /// @{ static constexpr siderust_frame_t frame_id() { @@ -106,7 +104,7 @@ struct Direction { } static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.azimuth_deg, c.polar_deg); + return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); } /// @} @@ -124,7 +122,7 @@ struct Direction { Direction> to_frame(const JulianDate& jd) const { if constexpr (std::is_same_v) { - return Direction(azimuth_.value(), polar_.value()); + return Direction(azimuth_, polar_); } else { siderust_spherical_dir_t out; check_status( @@ -193,11 +191,6 @@ struct Position { Position(qtty::Degree azimuth, qtty::Degree polar, U dist) : azimuth_(azimuth), polar_(polar), dist_(dist) {} - Position(double azimuth_deg, double polar_deg, double dist_val) - : azimuth_(qtty::Degree(azimuth_deg)), - polar_(qtty::Degree(polar_deg)), - dist_(U(dist_val)) {} - /// Extract the direction component. Direction direction() const { return Direction(azimuth_, polar_); @@ -233,5 +226,33 @@ struct Position { U distance() const { return dist_; } }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Direction with RA/Dec frames. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.ra() << ", " << dir.dec(); +} + +/** + * @brief Stream operator for Direction with Az/Alt frame. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.az() << ", " << dir.alt(); +} + +/** + * @brief Stream operator for Direction with Lon/Lat frames. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.lon() << ", " << dir.lat(); +} + } // namespace spherical } // namespace siderust diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index b031634..58704ca 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -15,6 +15,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include #include namespace siderust { @@ -301,4 +302,50 @@ inline bool is_waning(MoonPhaseLabel label) { } } +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for PhaseKind. + */ +inline std::ostream& operator<<(std::ostream& os, PhaseKind kind) { + switch (kind) { + case PhaseKind::NewMoon: + return os << "new moon"; + case PhaseKind::FirstQuarter: + return os << "first quarter"; + case PhaseKind::FullMoon: + return os << "full moon"; + case PhaseKind::LastQuarter: + return os << "last quarter"; + } + return os << "unknown"; +} + +/** + * @brief Stream operator for MoonPhaseLabel. + */ +inline std::ostream& operator<<(std::ostream& os, MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::NewMoon: + return os << "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return os << "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return os << "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return os << "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return os << "full moon"; + case MoonPhaseLabel::WaningGibbous: + return os << "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return os << "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return os << "waning crescent"; + } + return os << "unknown"; +} + } // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index dfc22cb..483bd6c 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,7 +13,7 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(279.23473, 38.78369); // Direction + * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); * * // Template-targeted transform — invalid pairs won't compile diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index 640a1d8..c7d6bc9 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -2,14 +2,30 @@ /** * @file target.hpp - * @brief RAII C++ wrapper for an siderust Target (fixed ICRS pointing). + * @brief Generic strongly-typed RAII wrapper for a siderust Target direction. * - * A `Target` represents a fixed celestial direction (RA, Dec at a given epoch) - * and exposes altitude and azimuth computations via the same observer/window - * API as the sun/moon/star helpers in altitude.hpp and azimuth.hpp. + * `Target` represents a fixed celestial direction in any supported + * reference frame and exposes altitude and azimuth computations via the same + * observer/window API as the sun/moon/star helpers in altitude.hpp and + * azimuth.hpp. * - * Proper motion is stored for future use but presently not applied during - * altitude/azimuth queries (epoch-fixed direction is used throughout). + * The template parameter `C` must be an instantiation of + * `spherical::Direction` for a frame `F` that can be transformed to ICRS + * (i.e., `frames::has_frame_transform_v` must be true). + * Non-ICRS directions are silently converted to ICRS at construction; the + * original typed direction is retained as C++ state. + * + * Supported frames: + * - `frames::ICRS`, `frames::ICRF` + * - `frames::EquatorialMeanJ2000`, `frames::EquatorialMeanOfDate`, + * `frames::EquatorialTrueOfDate` + * - `frames::EclipticMeanJ2000` + * + * Convenience aliases: + * - `ICRSTarget`, `ICRFTarget` + * - `EquatorialMeanJ2000Target`, `EquatorialMeanOfDateTarget`, + * `EquatorialTrueOfDateTarget` + * - `EclipticMeanJ2000Target` */ #include "altitude.hpp" @@ -18,37 +34,113 @@ #include "ffi_core.hpp" #include "time.hpp" #include "trackable.hpp" +#include #include #include namespace siderust { +// ============================================================================ +// Internal type traits +// ============================================================================ + +namespace detail { + +/// @cond INTERNAL + +/// True iff T is an instantiation of spherical::Direction. +template +struct is_spherical_direction : std::false_type {}; + +template +struct is_spherical_direction> : std::true_type {}; + +template +inline constexpr bool is_spherical_direction_v = is_spherical_direction::value; + +/// Extract the frame tag F from spherical::Direction. +template +struct spherical_direction_frame; // undefined primary + +template +struct spherical_direction_frame> { + using type = F; +}; + +template +using spherical_direction_frame_t = typename spherical_direction_frame::type; + +/// @endcond + +} // namespace detail + +// ============================================================================ +// Target +// ============================================================================ + /** - * @brief Move-only RAII handle for a siderust target direction. + * @brief Move-only RAII wrapper for a fixed celestial target direction. + * + * @tparam C Spherical direction type (e.g. `spherical::direction::ICRS`). * - * ### Example + * ### Example — ICRS target (Vega at J2000) * @code - * siderust::Target vega(279.2348, +38.7836, 2451545.0); // Vega at J2000 - * auto alt = vega.altitude_at(obs, now); + * using namespace siderust; + * ICRSTarget vega{ spherical::direction::ICRS{ 279.2348_deg, +38.7836_deg } }; + * auto alt = vega.altitude_at(obs, now); // → qtty::Degree + * std::cout << vega.ra() << "\n"; // qtty::Degree (equatorial frames) + * @endcode + * + * ### Example — Ecliptic target (auto-converted to ICRS internally) + * @code + * EclipticMeanJ2000Target ec{ + * spherical::direction::EclipticMeanJ2000{ 246.2_deg, 59.2_deg } }; + * auto alt = ec.altitude_at(obs, now); * @endcode */ +template class Target : public Trackable { + + static_assert(detail::is_spherical_direction_v, + "Target: C must be a specialisation of " + "siderust::spherical::Direction"); + + using Frame = detail::spherical_direction_frame_t; + + static_assert(frames::has_frame_transform_v, + "Target: frame F must support a transform to ICRS " + "(frames::has_frame_transform_v must be true). " + "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " + "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); + public: // ------------------------------------------------------------------ // Construction / destruction // ------------------------------------------------------------------ /** - * @brief Create a target from ICRS [RA, Dec] and an epoch. + * @brief Construct from a strongly-typed spherical direction. + * + * For frames other than ICRS, the direction is converted to ICRS before + * being registered with the Rust FFI. The original `C` direction is + * retained for C++-side accessors. * - * @param ra_deg Right ascension, degrees [0, 360). - * @param dec_deg Declination, degrees [−90, +90]. - * @param epoch_jd Julian Date of the coordinate epoch (default J2000.0). + * @param dir Spherical direction (any supported frame). + * @param epoch Coordinate epoch (default J2000.0). */ - explicit Target(double ra_deg, double dec_deg, double epoch_jd = 2451545.0) { + explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) + : m_dir_(dir), m_epoch_(epoch) { + // Convert to ICRS for the FFI; identity transform when already ICRS. + if constexpr (std::is_same_v) { + m_icrs_ = dir; + } else { + m_icrs_ = dir.template to_frame(epoch); + } SiderustTarget* h = nullptr; - check_status(siderust_target_create(ra_deg, dec_deg, epoch_jd, &h), - "Target::Target"); + check_status( + siderust_target_create( + m_icrs_.ra().value(), m_icrs_.dec().value(), epoch.value(), &h), + "Target::Target"); handle_ = h; } @@ -60,7 +152,11 @@ class Target : public Trackable { } /// Move constructor. - Target(Target&& other) noexcept : handle_(other.handle_) { + Target(Target&& other) noexcept + : m_dir_(std::move(other.m_dir_)), + m_epoch_(other.m_epoch_), + m_icrs_(other.m_icrs_), + handle_(other.handle_) { other.handle_ = nullptr; } @@ -70,6 +166,9 @@ class Target : public Trackable { if (handle_) { siderust_target_free(handle_); } + m_dir_ = std::move(other.m_dir_); + m_epoch_ = other.m_epoch_; + m_icrs_ = other.m_icrs_; handle_ = other.handle_; other.handle_ = nullptr; } @@ -84,40 +183,39 @@ class Target : public Trackable { // Coordinate accessors // ------------------------------------------------------------------ - /// Right ascension of the target (degrees). - double ra_deg() const { - double out{}; - check_status(siderust_target_ra_deg(handle_, &out), "Target::ra_deg"); - return out; - } + /// The original typed direction as supplied at construction. + const C& direction() const { return m_dir_; } - /// Declination of the target (degrees). - double dec_deg() const { - double out{}; - check_status(siderust_target_dec_deg(handle_, &out), "Target::dec_deg"); - return out; - } + /// Epoch of the coordinate. + JulianDate epoch() const { return m_epoch_; } - /// Epoch of the coordinates (Julian Date). - double epoch_jd() const { - double out{}; - check_status(siderust_target_epoch_jd(handle_, &out), "Target::epoch_jd"); - return out; - } + /// The ICRS direction used for FFI calls (equals `direction()` when C is + /// already `spherical::direction::ICRS`). + const spherical::direction::ICRS& icrs_direction() const { return m_icrs_; } + + /// Right ascension — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree ra() const { return m_dir_.ra(); } + + /// Declination — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree dec() const { return m_dir_.dec(); } // ------------------------------------------------------------------ - // Altitude queries + // Altitude queries (implements Trackable) // ------------------------------------------------------------------ /** * @brief Compute altitude (degrees) at a given MJD instant. + * + * @note The Rust FFI returns radians; this method converts to degrees. */ qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_altitude_at( handle_, obs.to_c(), mjd.value(), &out), "Target::altitude_at"); - return qtty::Degree(out); + return qtty::Radian(out).to(); } /** @@ -127,7 +225,7 @@ class Target : public Trackable { const Geodetic& obs, const Period& window, qtty::Degree threshold, const SearchOptions& opts = {}) const override { tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; + uintptr_t count = 0; check_status(siderust_target_above_threshold( handle_, obs.to_c(), window.c_inner(), threshold.value(), opts.to_c(), &ptr, &count), @@ -135,9 +233,7 @@ class Target : public Trackable { return detail_periods_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector above_threshold( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -150,11 +246,11 @@ class Target : public Trackable { std::vector below_threshold( const Geodetic& obs, const Period& window, qtty::Degree threshold, const SearchOptions& opts = {}) const override { - // Target wraps an ICRS direction; use icrs_below_threshold FFI. + // Always pass ICRS direction to the FFI layer. siderust_spherical_dir_t dir_c{}; - dir_c.polar_deg = dec_deg(); - dir_c.azimuth_deg = ra_deg(); - dir_c.frame = SIDERUST_FRAME_T_ICRS; + dir_c.polar_deg = m_icrs_.dec().value(); + dir_c.azimuth_deg = m_icrs_.ra().value(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; tempoch_period_mjd_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_icrs_below_threshold( @@ -164,9 +260,7 @@ class Target : public Trackable { return detail_periods_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector below_threshold( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -188,9 +282,7 @@ class Target : public Trackable { return detail::crossings_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector crossings( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -212,9 +304,7 @@ class Target : public Trackable { return detail::culminations_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector culminations( const Geodetic& obs, const MJD& start, const MJD& end, const SearchOptions& opts = {}) const { @@ -222,7 +312,7 @@ class Target : public Trackable { } // ------------------------------------------------------------------ - // Azimuth queries + // Azimuth queries (implements Trackable) // ------------------------------------------------------------------ /** @@ -251,9 +341,7 @@ class Target : public Trackable { return detail::az_crossings_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector azimuth_crossings( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree bearing, const SearchOptions& opts = {}) const { @@ -264,7 +352,10 @@ class Target : public Trackable { const SiderustTarget* c_handle() const { return handle_; } private: - SiderustTarget* handle_ = nullptr; + C m_dir_; + JulianDate m_epoch_; + spherical::direction::ICRS m_icrs_; + SiderustTarget* handle_ = nullptr; /// Build a Period vector from a tempoch_period_mjd_t* array. static std::vector detail_periods_from_c( @@ -279,4 +370,26 @@ class Target : public Trackable { } }; +// ============================================================================ +// Convenience type aliases +// ============================================================================ + +/// Fixed direction in ICRS (most common use-case). +using ICRSTarget = Target; + +/// Fixed direction in ICRF (treated identically to ICRS in Siderust). +using ICRFTarget = Target; + +/// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). +using EquatorialMeanJ2000Target = Target; + +/// Fixed direction in mean equatorial coordinates of date (precessed only). +using EquatorialMeanOfDateTarget = Target; + +/// Fixed direction in true equatorial coordinates of date (precessed + nutated). +using EquatorialTrueOfDateTarget = Target; + +/// Fixed direction in mean ecliptic coordinates of J2000.0. +using EclipticMeanJ2000Target = Target; + } // namespace siderust diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index cc24154..72b2419 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -120,7 +120,73 @@ TEST_F(AltitudeTest, IcrsAltitudeAt) { TEST_F(AltitudeTest, IcrsAboveThreshold) { const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - auto periods = icrs_altitude::above_threshold( - vega_icrs, obs, window, qtty::Degree(30.0)); + auto periods = icrs_altitude::above_threshold( + vega_icrs, obs, window, qtty::Degree(30.0)); EXPECT_GT(periods.size(), 0u); } + +// ============================================================================ +// Target — generic strongly-typed target +// ============================================================================ + +// Vega ICRS coordinates (J2000): RA=279.2348°, Dec=+38.7836° +TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + // altitude_at returns qtty::Degree (radian/degree bug-fix verification) + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); + // Vega should rise above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); +} + +TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); + EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); + // epoch defaults to J2000 + EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); + // icrs_direction is the same for an ICRS Target + EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); +} + +TEST_F(AltitudeTest, ICRSTargetPolymorphic) { + // Verify Target is usable through the Trackable interface + std::unique_ptr t = std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23), qtty::Degree(38.78) }); + qtty::Degree alt = t->altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { + // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° + EclipticMeanJ2000Target ec{ spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8) } }; + // ecl direction retained on the C++ side + EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); + EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); + // ICRS ra/dec computed at construction and accessible + EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); + EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); + // altitude should be a valid degree value + qtty::Degree alt = ec.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, EquatorialMeanJ2000TargetAltitudeAt) { + EquatorialMeanJ2000Target vega{ spherical::direction::EquatorialMeanJ2000{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 8cc6cc7..1e6e9d5 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -23,7 +23,7 @@ TEST(TypedCoordinates, AliasNamespaces) { TEST(TypedCoordinates, IcrsDirToEcliptic) { using namespace siderust::frames; - spherical::direction::ICRS vega(279.23473, 38.78369); + spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto jd = JulianDate::J2000(); // Compile-time typed transform: ICRS -> EclipticMeanJ2000 @@ -39,7 +39,7 @@ TEST(TypedCoordinates, IcrsDirToEcliptic) { TEST(TypedCoordinates, IcrsDirRoundtrip) { using namespace siderust::frames; - spherical::direction::ICRS icrs(100.0, 30.0); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto ecl = icrs.to_frame(jd); @@ -53,7 +53,7 @@ TEST(TypedCoordinates, IcrsDirRoundtrip) { TEST(TypedCoordinates, ToShorthand) { using namespace siderust::frames; - spherical::direction::ICRS icrs(100.0, 30.0); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); // .to(jd) is a shorthand for .to_frame(jd) @@ -65,7 +65,7 @@ TEST(TypedCoordinates, ToShorthand) { TEST(TypedCoordinates, IcrsDirToHorizontal) { using namespace siderust::frames; - spherical::direction::ICRS vega(279.23473, 38.78369); + spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto obs = ROQUE_DE_LOS_MUCHACHOS; @@ -79,7 +79,7 @@ TEST(TypedCoordinates, IcrsDirToHorizontal) { TEST(TypedCoordinates, EquatorialToIcrs) { using namespace siderust::frames; - spherical::direction::EquatorialMeanJ2000 eq(100.0, 30.0); + spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto icrs = eq.to_frame(jd); @@ -94,7 +94,7 @@ TEST(TypedCoordinates, MultiHopTransform) { using namespace siderust::frames; // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) - spherical::Direction mean_od(100.0, 30.0); + spherical::Direction mean_od(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto true_od = mean_od.to_frame(jd); @@ -108,7 +108,7 @@ TEST(TypedCoordinates, MultiHopTransform) { TEST(TypedCoordinates, SameFrameIdentity) { using namespace siderust::frames; - spherical::direction::ICRS icrs(123.456, -45.678); + spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); auto jd = JulianDate::J2000(); auto same = icrs.to_frame(jd); @@ -117,7 +117,7 @@ TEST(TypedCoordinates, SameFrameIdentity) { } TEST(TypedCoordinates, QttyDegreeAccessors) { - spherical::direction::ICRS d(123.456, -45.678); + spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); // Frame-specific getters for ICRS. qtty::Degree ra = d.ra(); From 716d7348c3d662de11e68423faa942196d120452 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 01:34:31 +0100 Subject: [PATCH 6/6] Refactor test cases for consistency and readability - Adjusted formatting in test files to improve readability by aligning code and comments. - Replaced multiple lines of code with more concise versions in tests for TypedCoordinates, Ephemeris, Observatories, and Time. - Ensured consistent use of spacing and indentation across all test cases. - Verified that all assertions and static assertions maintain their intended functionality. --- examples/altitude_events_example.cpp | 121 +-- examples/azimuth_lunar_phase_example.cpp | 102 +-- examples/coordinate_systems_example.cpp | 50 +- examples/coordinates_examples.cpp | 62 +- examples/demo.cpp | 169 +++-- examples/solar_system_bodies_example.cpp | 104 ++- examples/trackable_targets_example.cpp | 117 +-- include/siderust/altitude.hpp | 698 +++++++++--------- include/siderust/azimuth.hpp | 420 ++++++----- include/siderust/bodies.hpp | 327 ++++---- include/siderust/body_target.hpp | 340 ++++----- include/siderust/centers.hpp | 71 +- include/siderust/coordinates/cartesian.hpp | 97 +-- include/siderust/coordinates/conversions.hpp | 21 +- include/siderust/coordinates/geodetic.hpp | 60 +- include/siderust/coordinates/spherical.hpp | 416 ++++++----- .../types/cartesian/position/ecliptic.hpp | 12 +- .../types/spherical/direction/equatorial.hpp | 6 +- .../types/spherical/position/ecliptic.hpp | 3 +- include/siderust/ephemeris.hpp | 47 +- include/siderust/ffi_core.hpp | 197 ++--- include/siderust/frames.hpp | 194 +++-- include/siderust/lunar_phase.hpp | 330 +++++---- include/siderust/observatories.hpp | 39 +- include/siderust/siderust.hpp | 15 +- include/siderust/star_target.hpp | 123 +-- include/siderust/target.hpp | 563 +++++++------- include/siderust/time.hpp | 10 +- include/siderust/trackable.hpp | 122 +-- tests/main.cpp | 6 +- tests/test_altitude.cpp | 218 +++--- tests/test_bodies.cpp | 246 +++--- tests/test_coordinates.cpp | 216 +++--- tests/test_ephemeris.cpp | 66 +- tests/test_observatories.cpp | 40 +- tests/test_time.cpp | 122 +-- 36 files changed, 2938 insertions(+), 2812 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index ee88acf..f89a497 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -13,67 +13,72 @@ namespace { -void print_periods(const std::vector& periods, std::size_t limit) { - const std::size_t n = std::min(periods.size(), limit); - for (std::size_t i = 0; i < n; ++i) { - const auto& p = periods[i]; - std::cout << " " << (i + 1) << ") " - << p.start().to_utc() << " -> " << p.end().to_utc() - << " (" << std::fixed << std::setprecision(2) - << p.duration().value() << " h)\n"; - } +void print_periods(const std::vector &periods, + std::size_t limit) { + const std::size_t n = std::min(periods.size(), limit); + for (std::size_t i = 0; i < n; ++i) { + const auto &p = periods[i]; + std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " + << p.end().to_utc() << " (" << std::fixed << std::setprecision(2) + << p.duration().value() << " h)\n"; + } } } // namespace int main() { - using namespace siderust; - using namespace qtty::literals; - - const Geodetic obs = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(2.0); - const Period window(start, end); - - SearchOptions opts; - opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step - - std::cout << "=== altitude_events_example ===\n"; - std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - - auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); - std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; - print_periods(sun_nights, 3); - - auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); - std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; - if (!sun_cross.empty()) { - const auto& c = sun_cross.front(); - std::cout << " First crossing: " << c.time.to_utc() - << " (" << c.direction << ")\n"; - } - - auto moon_culm = moon::culminations(obs, window, opts); - std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; - if (!moon_culm.empty()) { - const auto& c = moon_culm.front(); - std::cout << " First culmination: " << c.time.to_utc() - << " kind=" << c.kind - << " alt=" << c.altitude - << std::endl; - } - - auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); - std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; - print_periods(vega_periods, 2); - - spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); - auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); - std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - - ICRSTarget fixed_target{ spherical::direction::ICRS{279.23473_deg, 38.78369_deg } }; - auto fixed_target_periods = fixed_target.above_threshold(obs, window, 45.0_deg, opts); - std::cout << "ICRSTarget::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; - - return 0; + using namespace siderust; + using namespace qtty::literals; + + const Geodetic obs = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(2.0); + const Period window(start, end); + + SearchOptions opts; + opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step + + std::cout << "=== altitude_events_example ===\n"; + std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + + auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); + std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() + << " period(s)\n"; + print_periods(sun_nights, 3); + + auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); + std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; + if (!sun_cross.empty()) { + const auto &c = sun_cross.front(); + std::cout << " First crossing: " << c.time.to_utc() << " (" << c.direction + << ")\n"; + } + + auto moon_culm = moon::culminations(obs, window, opts); + std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; + if (!moon_culm.empty()) { + const auto &c = moon_culm.front(); + std::cout << " First culmination: " << c.time.to_utc() + << " kind=" << c.kind << " alt=" << c.altitude << std::endl; + } + + auto vega_periods = + star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); + std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; + print_periods(vega_periods, 2); + + spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); + auto dir_visible = + icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); + std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() + << " period(s)\n"; + + ICRSTarget fixed_target{ + spherical::direction::ICRS{279.23473_deg, 38.78369_deg}}; + auto fixed_target_periods = + fixed_target.above_threshold(obs, window, 45.0_deg, opts); + std::cout << "ICRSTarget::above_threshold(45 deg): " + << fixed_target_periods.size() << " period(s)\n"; + + return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp index f07d006..d17f6a9 100644 --- a/examples/azimuth_lunar_phase_example.cpp +++ b/examples/azimuth_lunar_phase_example.cpp @@ -11,67 +11,69 @@ #include -namespace { -} // namespace +namespace {} // namespace int main() { - using namespace siderust; - using namespace qtty::literals; + using namespace siderust; + using namespace qtty::literals; - const Geodetic site = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(3.0); - const Period window(start, end); + const Geodetic site = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(3.0); + const Period window(start, end); - std::cout << "=== azimuth_lunar_phase_example ===\n"; - std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + std::cout << "=== azimuth_lunar_phase_example ===\n"; + std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() + << "\n\n"; - const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); - std::cout << "Instant azimuth\n"; - std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; - std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; - std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) << std::endl; + const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); + std::cout << "Instant azimuth\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; + std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) + << std::endl; - auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); - auto sun_ext = sun::azimuth_extrema(site, window); - auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); + auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); + auto sun_ext = sun::azimuth_extrema(site, window); + auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); - std::cout << "Azimuth events\n"; - std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; - std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; - if (!sun_ext.empty()) { - const auto& e = sun_ext.front(); - std::cout << " first extremum " << e.kind - << " at " << e.time.to_utc() - << " az=" << e.azimuth - << std::endl; - } - std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; + std::cout << "Azimuth events\n"; + std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; + std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; + if (!sun_ext.empty()) { + const auto &e = sun_ext.front(); + std::cout << " first extremum " << e.kind << " at " << e.time.to_utc() + << " az=" << e.azimuth << std::endl; + } + std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() + << " period(s)\n\n"; - const JulianDate jd_now = now.to_jd(); - auto geo_phase = moon::phase_geocentric(jd_now); - auto topo_phase = moon::phase_topocentric(jd_now, site); - auto topo_label = moon::phase_label(topo_phase); + const JulianDate jd_now = now.to_jd(); + auto geo_phase = moon::phase_geocentric(jd_now); + auto topo_phase = moon::phase_topocentric(jd_now, site); + auto topo_label = moon::phase_label(topo_phase); - auto phase_events = moon::find_phase_events( - Period(start, start + qtty::Day(30.0))); - auto half_lit = moon::illumination_range(window, 0.45, 0.55); + auto phase_events = + moon::find_phase_events(Period(start, start + qtty::Day(30.0))); + auto half_lit = moon::illumination_range(window, 0.45, 0.55); - std::cout << "Lunar phase\n"; - std::cout << std::fixed << std::setprecision(3) - << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" - << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction - << " (" << topo_label << ")\n"; + std::cout << "Lunar phase\n"; + std::cout << std::fixed << std::setprecision(3) + << " Geocentric illuminated fraction: " + << geo_phase.illuminated_fraction << "\n" + << " Topocentric illuminated fraction: " + << topo_phase.illuminated_fraction << " (" << topo_label << ")\n"; - std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; - const std::size_t n = std::min(phase_events.size(), 4); - for (std::size_t i = 0; i < n; ++i) { - const auto& ev = phase_events[i]; - std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; - } + std::cout << " Principal phase events in next 30 days: " + << phase_events.size() << "\n"; + const std::size_t n = std::min(phase_events.size(), 4); + for (std::size_t i = 0; i < n; ++i) { + const auto &ev = phase_events[i]; + std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; + } - std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " - << half_lit.size() << "\n"; + std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " + << half_lit.size() << "\n"; - return 0; + return 0; } diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index fcefb5a..3a144e4 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -11,37 +11,37 @@ #include int main() { - using namespace siderust; - using namespace siderust::frames; + using namespace siderust; + using namespace siderust::frames; - std::cout << "=== coordinate_systems_example ===\n"; + std::cout << "=== coordinate_systems_example ===\n"; - static_assert(has_frame_transform_v); - static_assert(has_frame_transform_v); - static_assert(has_horizontal_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_horizontal_transform_v); - const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; - const auto ecef = observer.to_cartesian(); + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const auto ecef = observer.to_cartesian(); - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::Direction src(qtty::Degree(279.23473), qtty::Degree(38.78369)); - const auto ecl = src.to_frame(jd); - const auto mod = src.to_frame(jd); - const auto tod = mod.to_frame(jd); - const auto horiz = src.to_horizontal(jd, observer); + spherical::Direction src(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + const auto ecl = src.to_frame(jd); + const auto mod = src.to_frame(jd); + const auto tod = mod.to_frame(jd); + const auto horiz = src.to_horizontal(jd, observer); - std::cout << std::fixed << std::setprecision(6); - std::cout << "Observer: " << observer << std::endl; - std::cout << "Observer in ECEF: " << ecef << std::endl; + std::cout << std::fixed << std::setprecision(6); + std::cout << "Observer: " << observer << std::endl; + std::cout << "Observer in ECEF: " << ecef << std::endl; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " << src << "\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; + std::cout << " Horizontal az/alt : " << horiz << "\n"; - std::cout << "Frame transforms for Vega-like direction\n"; - std::cout << " ICRS RA/Dec : " << src << "\n"; - std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; - std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; - std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; - std::cout << " Horizontal az/alt : " << horiz << "\n"; - - return 0; + return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index d13ccff..ee6fdb8 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -11,46 +11,46 @@ #include int main() { - using namespace siderust; - using namespace qtty::literals; + using namespace siderust; + using namespace qtty::literals; - std::cout << "=== coordinates_examples ===\n"; + std::cout << "=== coordinates_examples ===\n"; - const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); - const auto ecef_m = site.to_cartesian(); - const auto ecef_km = site.to_cartesian(); + const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); + const auto ecef_m = site.to_cartesian(); + const auto ecef_km = site.to_cartesian(); - static_assert(std::is_same_v< - std::remove_cv_t, - cartesian::position::ECEF>); + static_assert(std::is_same_v, + cartesian::position::ECEF>); - std::cout << "Geodetic -> ECEF \n " - << site << "\n" - << ecef_m << "\n" - << "(" << ecef_km << ")\n"<< std::endl; + std::cout << "Geodetic -> ECEF \n " << site << "\n" + << ecef_m << "\n" + << "(" << ecef_km << ")\n" + << std::endl; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_true = vega_icrs.to_frame(jd); - auto vega_horiz = vega_icrs.to_horizontal(jd, site); + spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_true = vega_icrs.to_frame(jd); + auto vega_horiz = vega_icrs.to_horizontal(jd, site); - std::cout << "Direction transforms\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; - std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; - std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; - std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; + std::cout << "Direction transforms\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; + std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; + std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; + std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; - spherical::position::ICRS synthetic_star( - 210.0_deg, -12.0_deg, 4.2_au); + spherical::position::ICRS synthetic_star( + 210.0_deg, -12.0_deg, 4.2_au); - cartesian::position::EclipticMeanJ2000 earth = - ephemeris::earth_heliocentric(jd); + cartesian::position::EclipticMeanJ2000 earth = + ephemeris::earth_heliocentric(jd); - std::cout << "Typed positions\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance() << std::endl; - std::cout << " Earth heliocentric x: " << earth.x() << std::endl; + std::cout << "Typed positions\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance() + << std::endl; + std::cout << " Earth heliocentric x: " << earth.x() << std::endl; - return 0; + return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 5ea5d96..a0e8c23 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -13,92 +13,89 @@ #include -namespace { - -} // namespace +namespace {} // namespace int main() { - using namespace siderust; - - const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period next_day(now, now + qtty::Day(1.0)); - - std::cout << "=== siderust-cpp extended demo ===\n"; - std::cout << "Observer: " << site << "\n"; - std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() - << " UTC " << jd.to_utc() << "\n\n"; - - spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_hor = vega_icrs.to_horizontal(jd, site); - std::cout << "Typed coordinates\n"; - std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; - std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; - std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; - - qtty::Degree sun_alt = sun::altitude_at(site, now).to(); - qtty::Degree sun_az = sun::azimuth_at(site, now); - std::cout << "Sun instant\n"; - std::cout << " Altitude=" << sun_alt.value() << " deg" - << " Azimuth=" << sun_az.value() << " deg\n"; - - auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); - if (!sun_crossings.empty()) { - std::cout << " Next horizon crossing: " - << sun_crossings.front().time.to_utc() << " (" - << sun_crossings.front().direction - << ")\n"; - } - std::cout << "\n"; - - BodyTarget mars(Body::Mars); - ICRSTarget fixed_target{ spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369) } }; // Vega-like - - std::vector>> targets; - targets.push_back({"Sun", std::make_unique(Body::Sun)}); - targets.push_back({"Vega", std::make_unique(VEGA)}); - targets.push_back({"Fixed target", std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); - - std::cout << "Trackable polymorphism\n"; - for (const auto& entry : targets) { - const auto& name = entry.first; - const auto& obj = entry.second; - auto alt = obj->altitude_at(site, now); - auto az = obj->azimuth_at(site, now); - std::cout << " " << std::setw(12) << std::left << name - << " alt=" << std::setw(8) << alt - << " az=" << az << std::endl; - } - std::cout << " Mars altitude via BodyTarget: " - << mars.altitude_at(site, now).value() << " deg\n"; - std::cout << " Fixed Target altitude: " - << fixed_target.altitude_at(site, now).value() << " deg\n\n"; - - auto earth_helio = ephemeris::earth_heliocentric(jd); - auto moon_geo = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt( - moon_geo.x().value() * moon_geo.x().value() + - moon_geo.y().value() * moon_geo.y().value() + - moon_geo.z().value() * moon_geo.z().value()); - - std::cout << "Ephemeris\n"; - std::cout << " Earth heliocentric " << earth_helio << " AU\n"; - std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - - auto phase = moon::phase_topocentric(jd, site); - auto label = moon::phase_label(phase); - auto bright_periods = moon::illumination_above( - Period(now, now + qtty::Day(7.0)), 0.8); - - std::cout << "Lunar phase\n"; - std::cout << " Illuminated fraction=" << phase.illuminated_fraction - << " label=" << label << "\n"; - std::cout << " Bright-moon periods (next 7 days, k>=0.8): " - << bright_periods.size() << "\n"; - - return 0; + using namespace siderust; + + const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period next_day(now, now + qtty::Day(1.0)); + + std::cout << "=== siderust-cpp extended demo ===\n"; + std::cout << "Observer: " << site << "\n"; + std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() + << " UTC " << jd.to_utc() << "\n\n"; + + spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_hor = vega_icrs.to_horizontal(jd, site); + std::cout << "Typed coordinates\n"; + std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; + std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; + std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; + + qtty::Degree sun_alt = sun::altitude_at(site, now).to(); + qtty::Degree sun_az = sun::azimuth_at(site, now); + std::cout << "Sun instant\n"; + std::cout << " Altitude=" << sun_alt.value() << " deg" + << " Azimuth=" << sun_az.value() << " deg\n"; + + auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); + if (!sun_crossings.empty()) { + std::cout << " Next horizon crossing: " + << sun_crossings.front().time.to_utc() << " (" + << sun_crossings.front().direction << ")\n"; + } + std::cout << "\n"; + + BodyTarget mars(Body::Mars); + ICRSTarget fixed_target{spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)}}; // Vega-like + + std::vector>> targets; + targets.push_back({"Sun", std::make_unique(Body::Sun)}); + targets.push_back({"Vega", std::make_unique(VEGA)}); + targets.push_back( + {"Fixed target", std::make_unique(spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)})}); + + std::cout << "Trackable polymorphism\n"; + for (const auto &entry : targets) { + const auto &name = entry.first; + const auto &obj = entry.second; + auto alt = obj->altitude_at(site, now); + auto az = obj->azimuth_at(site, now); + std::cout << " " << std::setw(12) << std::left << name + << " alt=" << std::setw(8) << alt << " az=" << az << std::endl; + } + std::cout << " Mars altitude via BodyTarget: " + << mars.altitude_at(site, now).value() << " deg\n"; + std::cout << " Fixed Target altitude: " + << fixed_target.altitude_at(site, now).value() << " deg\n\n"; + + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + + moon_geo.y().value() * moon_geo.y().value() + + moon_geo.z().value() * moon_geo.z().value()); + + std::cout << "Ephemeris\n"; + std::cout << " Earth heliocentric " << earth_helio << " AU\n"; + std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; + + auto phase = moon::phase_topocentric(jd, site); + auto label = moon::phase_label(phase); + auto bright_periods = + moon::illumination_above(Period(now, now + qtty::Day(7.0)), 0.8); + + std::cout << "Lunar phase\n"; + std::cout << " Illuminated fraction=" << phase.illuminated_fraction + << " label=" << label << "\n"; + std::cout << " Bright-moon periods (next 7 days, k>=0.8): " + << bright_periods.size() << "\n"; + + return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index 51971eb..25c69d7 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -13,71 +13,69 @@ namespace { -const char* az_kind_name(siderust::AzimuthExtremumKind kind) { - using siderust::AzimuthExtremumKind; - switch (kind) { - case AzimuthExtremumKind::Max: - return "max"; - case AzimuthExtremumKind::Min: - return "min"; - } - return "unknown"; +const char *az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; } } // namespace int main() { - using namespace siderust; + using namespace siderust; - const Geodetic site = MAUNA_KEA; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period window(now, now + qtty::Day(2.0)); + const Geodetic site = MAUNA_KEA; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period window(now, now + qtty::Day(2.0)); - std::cout << "=== solar_system_bodies_example ===\n"; - std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; + std::cout << "=== solar_system_bodies_example ===\n"; + std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; - std::cout << "Planet catalog constants\n"; - std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" - << " radius=" << MERCURY.radius_km << " km\n"; - std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" - << " radius=" << EARTH.radius_km << " km\n"; - std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" - << " radius=" << JUPITER.radius_km << " km\n\n"; + std::cout << "Planet catalog constants\n"; + std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" + << " radius=" << MERCURY.radius_km << " km\n"; + std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" + << " radius=" << EARTH.radius_km << " km\n"; + std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" + << " radius=" << JUPITER.radius_km << " km\n\n"; - auto earth = ephemeris::earth_heliocentric(jd); - auto moon_pos = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt( - moon_pos.x().value() * moon_pos.x().value() + - moon_pos.y().value() * moon_pos.y().value() + - moon_pos.z().value() * moon_pos.z().value()); + auto earth = ephemeris::earth_heliocentric(jd); + auto moon_pos = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt(moon_pos.x().value() * moon_pos.x().value() + + moon_pos.y().value() * moon_pos.y().value() + + moon_pos.z().value() * moon_pos.z().value()); - std::cout << "Ephemeris\n"; - std::cout << std::fixed << std::setprecision(6) - << " Earth heliocentric x=" << earth.x().value() - << " AU y=" << earth.y().value() << " AU\n"; - std::cout << std::setprecision(2) - << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; + std::cout << "Ephemeris\n"; + std::cout << std::fixed << std::setprecision(6) + << " Earth heliocentric x=" << earth.x().value() + << " AU y=" << earth.y().value() << " AU\n"; + std::cout << std::setprecision(2) + << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, Body::Jupiter}; + std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, + Body::Jupiter}; - std::cout << "Body dispatch API at observer\n"; - for (Body b : tracked) { - auto alt = body::altitude_at(b, site, now).to(); - auto az = body::azimuth_at(b, site, now).to(); - std::cout << " body=" << static_cast(b) - << " alt=" << alt - << " az=" << az << std::endl; - } + std::cout << "Body dispatch API at observer\n"; + for (Body b : tracked) { + auto alt = body::altitude_at(b, site, now).to(); + auto az = body::azimuth_at(b, site, now).to(); + std::cout << " body=" << static_cast(b) << " alt=" << alt + << " az=" << az << std::endl; + } - auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); - if (!moon_extrema.empty()) { - const auto& e = moon_extrema.front(); - std::cout << "\nMoon azimuth extrema\n"; - std::cout << " first " << az_kind_name(e.kind) - << " at " << e.time.to_utc() - << " az=" << e.azimuth << std::endl; - } + auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); + if (!moon_extrema.empty()) { + const auto &e = moon_extrema.front(); + std::cout << "\nMoon azimuth extrema\n"; + std::cout << " first " << az_kind_name(e.kind) << " at " << e.time.to_utc() + << " az=" << e.azimuth << std::endl; + } - return 0; + return 0; } diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp index e8e33b3..2e578be 100644 --- a/examples/trackable_targets_example.cpp +++ b/examples/trackable_targets_example.cpp @@ -1,7 +1,8 @@ /** * @file trackable_targets_example.cpp * @example trackable_targets_example.cpp - * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * @brief Using Target, StarTarget, BodyTarget through Trackable + * polymorphism. * * Demonstrates the strongly-typed Target template with multiple frames: * - ICRSTarget — fixed direction in ICRS equatorial coordinates @@ -23,67 +24,69 @@ namespace { struct NamedTrackable { - std::string name; - std::unique_ptr object; + std::string name; + std::unique_ptr object; }; } // namespace int main() { - using namespace siderust; - - const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); - const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); - const Period window(now, now + qtty::Day(1.0)); - - std::cout << "=== trackable_targets_example ===\n"; - std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; - - // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. - ICRSTarget fixed_vega_like{ spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369) } }; - std::cout << "ICRSTarget metadata\n"; - std::cout << " RA/Dec=" << fixed_vega_like.direction() - << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; - - // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). - // Automatically converted to ICRS by the constructor. - EclipticMeanJ2000Target ecliptic_vega{ spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8) } }; - auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); - std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; - std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; - std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() << " (converted)\n"; - std::cout << " alt=" << alt_ecliptic << std::endl; - - std::vector catalog; - catalog.push_back({"Sun", std::make_unique(Body::Sun)}); - catalog.push_back({"Mars", std::make_unique(Body::Mars)}); - catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); - catalog.push_back({"Fixed Vega-like (ICRS)", std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); - - for (const auto& entry : catalog) { - const auto& t = entry.object; - auto alt = t->altitude_at(site, now); - auto az = t->azimuth_at(site, now); - - std::cout << std::left << std::setw(22) << entry.name << std::right - << " alt=" << std::setw(9) << alt - << " az=" << az << std::endl; - - auto crossings = t->crossings(site, window, qtty::Degree(0.0)); - if (!crossings.empty()) { - const auto& first = crossings.front(); - std::cout << " first horizon crossing: " << first.time.to_utc() - << " (" << first.direction << ")\n"; - } - - auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); - if (!az_cross.empty()) { - std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() << "\n"; - } + using namespace siderust; + + const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); + const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); + const Period window(now, now + qtty::Day(1.0)); + + std::cout << "=== trackable_targets_example ===\n"; + std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; + + // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. + ICRSTarget fixed_vega_like{spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)}}; + std::cout << "ICRSTarget metadata\n"; + std::cout << " RA/Dec=" << fixed_vega_like.direction() + << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; + + // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). + // Automatically converted to ICRS by the constructor. + EclipticMeanJ2000Target ecliptic_vega{spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8)}}; + auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); + std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; + std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; + std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() + << " (converted)\n"; + std::cout << " alt=" << alt_ecliptic << std::endl; + + std::vector catalog; + catalog.push_back({"Sun", std::make_unique(Body::Sun)}); + catalog.push_back({"Mars", std::make_unique(Body::Mars)}); + catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); + catalog.push_back({"Fixed Vega-like (ICRS)", + std::make_unique(spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)})}); + + for (const auto &entry : catalog) { + const auto &t = entry.object; + auto alt = t->altitude_at(site, now); + auto az = t->azimuth_at(site, now); + + std::cout << std::left << std::setw(22) << entry.name << std::right + << " alt=" << std::setw(9) << alt << " az=" << az << std::endl; + + auto crossings = t->crossings(site, window, qtty::Degree(0.0)); + if (!crossings.empty()) { + const auto &first = crossings.front(); + std::cout << " first horizon crossing: " << first.time.to_utc() << " (" + << first.direction << ")\n"; } - return 0; + auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); + if (!az_cross.empty()) { + std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() + << "\n"; + } + } + + return 0; } diff --git a/include/siderust/altitude.hpp b/include/siderust/altitude.hpp index aefee3c..00529f8 100644 --- a/include/siderust/altitude.hpp +++ b/include/siderust/altitude.hpp @@ -2,7 +2,8 @@ /** * @file altitude.hpp - * @brief Altitude computations for Sun, Moon, stars, and arbitrary ICRS directions. + * @brief Altitude computations for Sun, Moon, stars, and arbitrary ICRS + * directions. * * Wraps siderust-ffi's altitude API with exception-safe C++ types and * RAII-managed output arrays. @@ -24,25 +25,26 @@ namespace siderust { * @brief A threshold-crossing event (rising or setting). */ struct CrossingEvent { - MJD time; - CrossingDirection direction; + MJD time; + CrossingDirection direction; - static CrossingEvent from_c(const siderust_crossing_event_t& c) { - return {MJD(c.mjd), static_cast(c.direction)}; - } + static CrossingEvent from_c(const siderust_crossing_event_t &c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } }; /** * @brief A culmination (local altitude extremum) event. */ struct CulminationEvent { - MJD time; - qtty::Degree altitude; - CulminationKind kind; - - static CulminationEvent from_c(const siderust_culmination_event_t& c) { - return {MJD(c.mjd), qtty::Degree(c.altitude_deg), static_cast(c.kind)}; - } + MJD time; + qtty::Degree altitude; + CulminationKind kind; + + static CulminationEvent from_c(const siderust_culmination_event_t &c) { + return {MJD(c.mjd), qtty::Degree(c.altitude_deg), + static_cast(c.kind)}; + } }; // ============================================================================ @@ -53,28 +55,28 @@ struct CulminationEvent { * @brief Options for altitude search algorithms. */ struct SearchOptions { - double time_tolerance_days = 1e-9; - double scan_step_days = 0.0; - bool has_scan_step = false; - - SearchOptions() = default; - - /// Set a custom scan step. - SearchOptions& with_scan_step(double step) { - scan_step_days = step; - has_scan_step = true; - return *this; - } - - /// Set time tolerance. - SearchOptions& with_tolerance(double tol) { - time_tolerance_days = tol; - return *this; - } - - siderust_search_opts_t to_c() const { - return {time_tolerance_days, scan_step_days, has_scan_step}; - } + double time_tolerance_days = 1e-9; + double scan_step_days = 0.0; + bool has_scan_step = false; + + SearchOptions() = default; + + /// Set a custom scan step. + SearchOptions &with_scan_step(double step) { + scan_step_days = step; + has_scan_step = true; + return *this; + } + + /// Set time tolerance. + SearchOptions &with_tolerance(double tol) { + time_tolerance_days = tol; + return *this; + } + + siderust_search_opts_t to_c() const { + return {time_tolerance_days, scan_step_days, has_scan_step}; + } }; // ============================================================================ @@ -82,34 +84,37 @@ struct SearchOptions { // ============================================================================ namespace detail { -inline std::vector periods_from_c(tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period::from_c(ptr[i])); - } - siderust_periods_free(ptr, count); - return result; -} - -inline std::vector crossings_from_c(siderust_crossing_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(CrossingEvent::from_c(ptr[i])); - } - siderust_crossings_free(ptr, count); - return result; -} - -inline std::vector culminations_from_c(siderust_culmination_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(CulminationEvent::from_c(ptr[i])); - } - siderust_culminations_free(ptr, count); - return result; +inline std::vector periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period::from_c(ptr[i])); + } + siderust_periods_free(ptr, count); + return result; +} + +inline std::vector +crossings_from_c(siderust_crossing_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(CrossingEvent::from_c(ptr[i])); + } + siderust_crossings_free(ptr, count); + return result; +} + +inline std::vector +culminations_from_c(siderust_culmination_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(CulminationEvent::from_c(ptr[i])); + } + siderust_culminations_free(ptr, count); + return result; } } // namespace detail @@ -123,137 +128,145 @@ namespace sun { /** * @brief Compute the Sun's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_sun_altitude_at(obs.to_c(), mjd.value(), &out), - "sun::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_sun_altitude_at(obs.to_c(), mjd.value(), &out), + "sun::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when the Sun is above a threshold altitude. */ -inline std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_above_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_above_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "sun::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find periods when the Sun is below a threshold altitude. */ -inline std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_below_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_below_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "sun::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for the Sun. */ -inline std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_crossings( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_crossings(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, + &count), + "sun::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for the Sun. */ -inline std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_culminations( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "sun::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_culminations(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(obs, Period(start, end), opts); +inline std::vector +culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return culminations(obs, Period(start, end), opts); } /** * @brief Find periods when the Sun's altitude is within [min, max]. */ -inline std::vector altitude_periods( - const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_altitude_periods(q, &ptr, &count), - "sun::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_altitude_periods(q, &ptr, &count), + "sun::altitude_periods"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector altitude_periods( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_altitude_periods(q, &ptr, &count), - "sun::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_altitude_periods(q, &ptr, &count), + "sun::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace sun @@ -267,137 +280,145 @@ namespace moon { /** * @brief Compute the Moon's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_moon_altitude_at(obs.to_c(), mjd.value(), &out), - "moon::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_moon_altitude_at(obs.to_c(), mjd.value(), &out), + "moon::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when the Moon is above a threshold altitude. */ -inline std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_above_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_above_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "moon::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find periods when the Moon is below a threshold altitude. */ -inline std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_below_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_below_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "moon::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for the Moon. */ -inline std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_crossings( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_crossings(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, + &count), + "moon::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for the Moon. */ -inline std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_culminations( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "moon::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_culminations(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(obs, Period(start, end), opts); +inline std::vector +culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return culminations(obs, Period(start, end), opts); } /** * @brief Find periods when the Moon's altitude is within [min, max]. */ -inline std::vector altitude_periods( - const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_altitude_periods(q, &ptr, &count), - "moon::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_altitude_periods(q, &ptr, &count), + "moon::altitude_periods"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector altitude_periods( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_altitude_periods(q, &ptr, &count), - "moon::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_altitude_periods(q, &ptr, &count), + "moon::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace moon @@ -411,108 +432,115 @@ namespace star_altitude { /** * @brief Compute a star's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Star& s, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_star_altitude_at( - s.c_handle(), obs.to_c(), mjd.value(), &out), - "star_altitude::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Star &s, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status( + siderust_star_altitude_at(s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when a star is above a threshold altitude. */ -inline std::vector above_threshold( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_above_threshold( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_above_threshold( + s.c_handle(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "star_altitude::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(s, obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(s, obs, Period(start, end), threshold, opts); } /** * @brief Find periods when a star is below a threshold altitude. */ -inline std::vector below_threshold( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_below_threshold( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_below_threshold( + s.c_handle(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "star_altitude::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(s, obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(s, obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for a star. */ -inline std::vector crossings( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_crossings( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_crossings(s.c_handle(), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "star_altitude::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(s, obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(s, obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for a star. */ -inline std::vector culminations( - const Star& s, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_culminations( - s.c_handle(), obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "star_altitude::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Star &s, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_culminations(s.c_handle(), obs.to_c(), + window.c_inner(), opts.to_c(), &ptr, + &count), + "star_altitude::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(s, obs, Period(start, end), opts); +inline std::vector +culminations(const Star &s, const Geodetic &obs, const MJD &start, + const MJD &end, const SearchOptions &opts = {}) { + return culminations(s, obs, Period(start, end), opts); } } // namespace star_altitude @@ -526,83 +554,77 @@ namespace icrs_altitude { /** * @brief Compute altitude (radians) for a fixed ICRS direction. */ -inline qtty::Radian altitude_at(const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_icrs_altitude_at( - dir.to_c(), obs.to_c(), mjd.value(), &out), - "icrs_altitude::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const spherical::direction::ICRS &dir, + const Geodetic &obs, const MJD &mjd) { + double out; + check_status( + siderust_icrs_altitude_at(dir.to_c(), obs.to_c(), mjd.value(), &out), + "icrs_altitude::altitude_at"); + return qtty::Radian(out); } /** * @brief Backward-compatible RA/Dec overload. */ inline qtty::Radian altitude_at(qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& mjd) { - return altitude_at(spherical::direction::ICRS(ra, dec), obs, mjd); + const Geodetic &obs, const MJD &mjd) { + return altitude_at(spherical::direction::ICRS(ra, dec), obs, mjd); } /** * @brief Find periods when a fixed ICRS direction is above a threshold. */ -inline std::vector above_threshold( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_above_threshold( - dir.to_c(), obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector +above_threshold(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_above_threshold( + dir.to_c(), obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec + [start, end] overload. */ -inline std::vector above_threshold( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold( - spherical::direction::ICRS(ra, dec), - obs, - Period(start, end), - threshold, - opts); +inline std::vector above_threshold(qtty::Degree ra, qtty::Degree dec, + const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(spherical::direction::ICRS(ra, dec), obs, + Period(start, end), threshold, opts); } /** * @brief Find periods when a fixed ICRS direction is below a threshold. */ -inline std::vector below_threshold( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_below_threshold( - dir.to_c(), obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector +below_threshold(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir.to_c(), obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec + [start, end] overload. */ -inline std::vector below_threshold( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold( - spherical::direction::ICRS(ra, dec), - obs, - Period(start, end), - threshold, - opts); +inline std::vector below_threshold(qtty::Degree ra, qtty::Degree dec, + const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(spherical::direction::ICRS(ra, dec), obs, + Period(start, end), threshold, opts); } } // namespace icrs_altitude diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 4f88f40..37640bd 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -2,18 +2,19 @@ /** * @file azimuth.hpp - * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS directions. + * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS + * directions. * * Wraps siderust-ffi's azimuth API with exception-safe C++ types and * RAII-managed output arrays. * * ### Covered computations - * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | in_azimuth_range | + * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | + * in_azimuth_range | * |---------|:----------:|:-----------------:|:---------------:|:----------------:| - * | Sun | ✓ | ✓ | ✓ | ✓ | - * | Moon | ✓ | ✓ | ✓ | ✓ | - * | Star | ✓ | ✓ | – | – | - * | ICRS | ✓ | – | – | – | + * | Sun | ✓ | ✓ | ✓ | ✓ | | Moon | ✓ + * | ✓ | ✓ | ✓ | | Star | ✓ | ✓ + * | – | – | | ICRS | ✓ | – | – | – | */ #include "altitude.hpp" @@ -34,34 +35,36 @@ namespace siderust { * @brief Distinguishes azimuth extrema: northernmost or southernmost bearing. */ enum class AzimuthExtremumKind : int32_t { - Max = 0, ///< Northernmost (or easternmost) direction reached by the body. - Min = 1, ///< Southernmost (or westernmost) direction reached by the body. + Max = 0, ///< Northernmost (or easternmost) direction reached by the body. + Min = 1, ///< Southernmost (or westernmost) direction reached by the body. }; /** * @brief An azimuth bearing-crossing event. */ struct AzimuthCrossingEvent { - MJD time; ///< Epoch of the crossing (MJD). - CrossingDirection direction; ///< Whether the azimuth is increasing or decreasing. - - static AzimuthCrossingEvent from_c(const siderust_azimuth_crossing_event_t& c) { - return {MJD(c.mjd), static_cast(c.direction)}; - } + MJD time; ///< Epoch of the crossing (MJD). + CrossingDirection + direction; ///< Whether the azimuth is increasing or decreasing. + + static AzimuthCrossingEvent + from_c(const siderust_azimuth_crossing_event_t &c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } }; /** * @brief An azimuth extremum event. */ struct AzimuthExtremum { - MJD time; ///< Epoch of the extremum (MJD). - qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). - AzimuthExtremumKind kind; ///< Maximum or minimum. - - static AzimuthExtremum from_c(const siderust_azimuth_extremum_t& c) { - return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), - static_cast(c.kind)}; - } + MJD time; ///< Epoch of the extremum (MJD). + qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). + AzimuthExtremumKind kind; ///< Maximum or minimum. + + static AzimuthExtremum from_c(const siderust_azimuth_extremum_t &c) { + return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), + static_cast(c.kind)}; + } }; // ============================================================================ @@ -69,26 +72,26 @@ struct AzimuthExtremum { // ============================================================================ namespace detail { -inline std::vector az_crossings_from_c( - siderust_azimuth_crossing_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); - } - siderust_azimuth_crossings_free(ptr, count); - return result; +inline std::vector +az_crossings_from_c(siderust_azimuth_crossing_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); + } + siderust_azimuth_crossings_free(ptr, count); + return result; } -inline std::vector az_extrema_from_c( - siderust_azimuth_extremum_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(AzimuthExtremum::from_c(ptr[i])); - } - siderust_azimuth_extrema_free(ptr, count); - return result; +inline std::vector +az_extrema_from_c(siderust_azimuth_extremum_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthExtremum::from_c(ptr[i])); + } + siderust_azimuth_extrema_free(ptr, count); + return result; } } // namespace detail @@ -100,88 +103,91 @@ inline std::vector az_extrema_from_c( namespace sun { /** - * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), - "sun::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), + "sun::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when the Sun crosses a given bearing. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_azimuth_crossings( - obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "sun::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_crossings(obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), + &ptr, &count), + "sun::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, const SearchOptions &opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); } /** * @brief Find azimuth extrema (northernmost / southernmost) for the Sun. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_azimuth_extrema( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "sun::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_extrema(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return azimuth_extrema(obs, Period(start, end), opts); +inline std::vector +azimuth_extrema(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); } /** - * @brief Find periods when the Sun's azimuth is within [min_bearing, max_bearing]. + * @brief Find periods when the Sun's azimuth is within [min_bearing, + * max_bearing]. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const Period& window, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_in_azimuth_range( - obs.to_c(), window.c_inner(), - min_bearing.value(), max_bearing.value(), - opts.to_c(), &ptr, &count), - "sun::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(const Geodetic &obs, + const Period &window, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_in_azimuth_range( + obs.to_c(), window.c_inner(), min_bearing.value(), + max_bearing.value(), opts.to_c(), &ptr, &count), + "sun::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +inline std::vector in_azimuth_range(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, + opts); } } // namespace sun @@ -193,88 +199,91 @@ inline std::vector in_azimuth_range( namespace moon { /** - * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), - "moon::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), + "moon::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when the Moon crosses a given bearing. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_azimuth_crossings( - obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "moon::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_crossings(obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), + &ptr, &count), + "moon::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, const SearchOptions &opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); } /** * @brief Find azimuth extrema (northernmost / southernmost) for the Moon. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_azimuth_extrema( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "moon::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_extrema(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return azimuth_extrema(obs, Period(start, end), opts); +inline std::vector +azimuth_extrema(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); } /** - * @brief Find periods when the Moon's azimuth is within [min_bearing, max_bearing]. + * @brief Find periods when the Moon's azimuth is within [min_bearing, + * max_bearing]. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const Period& window, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_in_azimuth_range( - obs.to_c(), window.c_inner(), - min_bearing.value(), max_bearing.value(), - opts.to_c(), &ptr, &count), - "moon::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(const Geodetic &obs, + const Period &window, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_in_azimuth_range( + obs.to_c(), window.c_inner(), min_bearing.value(), + max_bearing.value(), opts.to_c(), &ptr, &count), + "moon::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +inline std::vector in_azimuth_range(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, + opts); } } // namespace moon @@ -286,38 +295,41 @@ inline std::vector in_azimuth_range( namespace star_altitude { /** - * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Star& s, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_star_azimuth_at( - s.c_handle(), obs.to_c(), mjd.value(), &out), - "star_altitude::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Star &s, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status( + siderust_star_azimuth_at(s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when a star crosses a given azimuth bearing. */ -inline std::vector azimuth_crossings( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_azimuth_crossings( - s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "star_altitude::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Star &s, const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_azimuth_crossings( + s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "star_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(s, obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Star &s, const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(s, obs, Period(start, end), bearing, opts); } } // namespace star_altitude @@ -331,58 +343,58 @@ namespace icrs_altitude { /** * @brief Compute azimuth (degrees, N-clockwise) for a fixed ICRS direction. */ -inline qtty::Degree azimuth_at(const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_icrs_azimuth_at( - dir.to_c(), - obs.to_c(), mjd.value(), &out), - "icrs_altitude::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const spherical::direction::ICRS &dir, + const Geodetic &obs, const MJD &mjd) { + double out; + check_status( + siderust_icrs_azimuth_at(dir.to_c(), obs.to_c(), mjd.value(), &out), + "icrs_altitude::azimuth_at"); + return qtty::Degree(out); } /** * @brief Backward-compatible RA/Dec overload. */ inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& mjd) { - return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); + const Geodetic &obs, const MJD &mjd) { + return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); } /** * @brief Find epochs when an ICRS direction crosses a given azimuth bearing. */ -inline std::vector azimuth_crossings( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_azimuth_crossings( - dir.to_c(), obs.to_c(), window.c_inner(), - bearing.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree bearing, + const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_azimuth_crossings( + dir.to_c(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec overload. */ -inline std::vector azimuth_crossings( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, bearing, opts); +inline std::vector +azimuth_crossings(qtty::Degree ra, qtty::Degree dec, const Geodetic &obs, + const Period &window, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, + bearing, opts); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const spherical::direction::ICRS &dir, const Geodetic &obs, + const MJD &start, const MJD &end, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); } } // namespace icrs_altitude @@ -394,14 +406,14 @@ inline std::vector azimuth_crossings( /** * @brief Stream operator for AzimuthExtremumKind. */ -inline std::ostream& operator<<(std::ostream& os, AzimuthExtremumKind kind) { - switch (kind) { - case AzimuthExtremumKind::Max: - return os << "max"; - case AzimuthExtremumKind::Min: - return os << "min"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, AzimuthExtremumKind kind) { + switch (kind) { + case AzimuthExtremumKind::Max: + return os << "max"; + case AzimuthExtremumKind::Min: + return os << "min"; + } + return os << "unknown"; } } // namespace siderust diff --git a/include/siderust/bodies.hpp b/include/siderust/bodies.hpp index cbc3247..f6f2fa3 100644 --- a/include/siderust/bodies.hpp +++ b/include/siderust/bodies.hpp @@ -21,18 +21,18 @@ namespace siderust { * @brief Proper motion for a star (equatorial). */ struct ProperMotion { - double pm_ra_deg_yr; ///< RA proper motion (deg/yr). - double pm_dec_deg_yr; ///< Dec proper motion (deg/yr). - RaConvention convention; ///< RA rate convention. - - ProperMotion(double ra, double dec, - RaConvention conv = RaConvention::MuAlphaStar) - : pm_ra_deg_yr(ra), pm_dec_deg_yr(dec), convention(conv) {} - - siderust_proper_motion_t to_c() const { - return {pm_ra_deg_yr, pm_dec_deg_yr, - static_cast(convention)}; - } + double pm_ra_deg_yr; ///< RA proper motion (deg/yr). + double pm_dec_deg_yr; ///< Dec proper motion (deg/yr). + RaConvention convention; ///< RA rate convention. + + ProperMotion(double ra, double dec, + RaConvention conv = RaConvention::MuAlphaStar) + : pm_ra_deg_yr(ra), pm_dec_deg_yr(dec), convention(conv) {} + + siderust_proper_motion_t to_c() const { + return {pm_ra_deg_yr, pm_dec_deg_yr, + static_cast(convention)}; + } }; // ============================================================================ @@ -43,19 +43,23 @@ struct ProperMotion { * @brief Keplerian orbital elements. */ struct Orbit { - double semi_major_axis_au; - double eccentricity; - double inclination_deg; - double lon_ascending_node_deg; - double arg_perihelion_deg; - double mean_anomaly_deg; - double epoch_jd; - - static Orbit from_c(const siderust_orbit_t& c) { - return {c.semi_major_axis_au, c.eccentricity, c.inclination_deg, - c.lon_ascending_node_deg, c.arg_perihelion_deg, - c.mean_anomaly_deg, c.epoch_jd}; - } + double semi_major_axis_au; + double eccentricity; + double inclination_deg; + double lon_ascending_node_deg; + double arg_perihelion_deg; + double mean_anomaly_deg; + double epoch_jd; + + static Orbit from_c(const siderust_orbit_t &c) { + return {c.semi_major_axis_au, + c.eccentricity, + c.inclination_deg, + c.lon_ascending_node_deg, + c.arg_perihelion_deg, + c.mean_anomaly_deg, + c.epoch_jd}; + } }; // ============================================================================ @@ -66,74 +70,74 @@ struct Orbit { * @brief Planet data (value type, copyable). */ struct Planet { - double mass_kg; - double radius_km; - Orbit orbit; + double mass_kg; + double radius_km; + Orbit orbit; - static Planet from_c(const siderust_planet_t& c) { - return {c.mass_kg, c.radius_km, Orbit::from_c(c.orbit)}; - } + static Planet from_c(const siderust_planet_t &c) { + return {c.mass_kg, c.radius_km, Orbit::from_c(c.orbit)}; + } }; namespace detail { inline Planet make_planet_mercury() { - siderust_planet_t out; - check_status(siderust_planet_mercury(&out), "MERCURY"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_mercury(&out), "MERCURY"); + return Planet::from_c(out); } inline Planet make_planet_venus() { - siderust_planet_t out; - check_status(siderust_planet_venus(&out), "VENUS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_venus(&out), "VENUS"); + return Planet::from_c(out); } inline Planet make_planet_earth() { - siderust_planet_t out; - check_status(siderust_planet_earth(&out), "EARTH"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_earth(&out), "EARTH"); + return Planet::from_c(out); } inline Planet make_planet_mars() { - siderust_planet_t out; - check_status(siderust_planet_mars(&out), "MARS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_mars(&out), "MARS"); + return Planet::from_c(out); } inline Planet make_planet_jupiter() { - siderust_planet_t out; - check_status(siderust_planet_jupiter(&out), "JUPITER"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_jupiter(&out), "JUPITER"); + return Planet::from_c(out); } inline Planet make_planet_saturn() { - siderust_planet_t out; - check_status(siderust_planet_saturn(&out), "SATURN"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_saturn(&out), "SATURN"); + return Planet::from_c(out); } inline Planet make_planet_uranus() { - siderust_planet_t out; - check_status(siderust_planet_uranus(&out), "URANUS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_uranus(&out), "URANUS"); + return Planet::from_c(out); } inline Planet make_planet_neptune() { - siderust_planet_t out; - check_status(siderust_planet_neptune(&out), "NEPTUNE"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_neptune(&out), "NEPTUNE"); + return Planet::from_c(out); } } // namespace detail inline const Planet MERCURY = detail::make_planet_mercury(); -inline const Planet VENUS = detail::make_planet_venus(); -inline const Planet EARTH = detail::make_planet_earth(); -inline const Planet MARS = detail::make_planet_mars(); +inline const Planet VENUS = detail::make_planet_venus(); +inline const Planet EARTH = detail::make_planet_earth(); +inline const Planet MARS = detail::make_planet_mars(); inline const Planet JUPITER = detail::make_planet_jupiter(); -inline const Planet SATURN = detail::make_planet_saturn(); -inline const Planet URANUS = detail::make_planet_uranus(); +inline const Planet SATURN = detail::make_planet_saturn(); +inline const Planet URANUS = detail::make_planet_uranus(); inline const Planet NEPTUNE = detail::make_planet_neptune(); // Backward-compatible function aliases. @@ -156,113 +160,110 @@ inline Planet neptune() { return NEPTUNE; } * Non-copyable; move-only. Released on destruction. */ class Star { - SiderustStar* m_handle = nullptr; - - explicit Star(SiderustStar* h) : m_handle(h) {} - - public: - Star() = default; - ~Star() { - if (m_handle) - siderust_star_free(m_handle); + SiderustStar *m_handle = nullptr; + + explicit Star(SiderustStar *h) : m_handle(h) {} + +public: + Star() = default; + ~Star() { + if (m_handle) + siderust_star_free(m_handle); + } + + // Move-only + Star(Star &&o) noexcept : m_handle(o.m_handle) { o.m_handle = nullptr; } + Star &operator=(Star &&o) noexcept { + if (this != &o) { + if (m_handle) + siderust_star_free(m_handle); + m_handle = o.m_handle; + o.m_handle = nullptr; } - - // Move-only - Star(Star&& o) noexcept : m_handle(o.m_handle) { o.m_handle = nullptr; } - Star& operator=(Star&& o) noexcept { - if (this != &o) { - if (m_handle) - siderust_star_free(m_handle); - m_handle = o.m_handle; - o.m_handle = nullptr; - } - return *this; + return *this; + } + Star(const Star &) = delete; + Star &operator=(const Star &) = delete; + + /// Whether the handle is valid. + explicit operator bool() const { return m_handle != nullptr; } + + /// Access the raw C handle (for passing to altitude functions). + const SiderustStar *c_handle() const { return m_handle; } + + // -- Factory methods -- + + /** + * @brief Look up a star from the built-in catalog. + * + * Supported: "VEGA", "SIRIUS", "POLARIS", "CANOPUS", "ARCTURUS", + * "RIGEL", "BETELGEUSE", "PROCYON", "ALDEBARAN", "ALTAIR". + */ + static Star catalog(const std::string &name) { + SiderustStar *h = nullptr; + check_status(siderust_star_catalog(name.c_str(), &h), "Star::catalog"); + return Star(h); + } + + /** + * @brief Create a custom star. + * + * @param name Star name. + * @param distance_ly Distance in light-years. + * @param mass_solar Mass in solar masses. + * @param radius_solar Radius in solar radii. + * @param luminosity_solar Luminosity in solar luminosities. + * @param ra_deg Right ascension (J2000) in degrees. + * @param dec_deg Declination (J2000) in degrees. + * @param epoch_jd Epoch of coordinates (Julian Date). + * @param pm Optional proper motion. + */ + static Star create(const std::string &name, double distance_ly, + double mass_solar, double radius_solar, + double luminosity_solar, double ra_deg, double dec_deg, + double epoch_jd, + const std::optional &pm = std::nullopt) { + SiderustStar *h = nullptr; + const siderust_proper_motion_t *pm_ptr = nullptr; + siderust_proper_motion_t pm_c{}; + if (pm.has_value()) { + pm_c = pm->to_c(); + pm_ptr = &pm_c; } - Star(const Star&) = delete; - Star& operator=(const Star&) = delete; - - /// Whether the handle is valid. - explicit operator bool() const { return m_handle != nullptr; } - - /// Access the raw C handle (for passing to altitude functions). - const SiderustStar* c_handle() const { return m_handle; } - - // -- Factory methods -- - - /** - * @brief Look up a star from the built-in catalog. - * - * Supported: "VEGA", "SIRIUS", "POLARIS", "CANOPUS", "ARCTURUS", - * "RIGEL", "BETELGEUSE", "PROCYON", "ALDEBARAN", "ALTAIR". - */ - static Star catalog(const std::string& name) { - SiderustStar* h = nullptr; - check_status(siderust_star_catalog(name.c_str(), &h), - "Star::catalog"); - return Star(h); - } - - /** - * @brief Create a custom star. - * - * @param name Star name. - * @param distance_ly Distance in light-years. - * @param mass_solar Mass in solar masses. - * @param radius_solar Radius in solar radii. - * @param luminosity_solar Luminosity in solar luminosities. - * @param ra_deg Right ascension (J2000) in degrees. - * @param dec_deg Declination (J2000) in degrees. - * @param epoch_jd Epoch of coordinates (Julian Date). - * @param pm Optional proper motion. - */ - static Star create(const std::string& name, - double distance_ly, - double mass_solar, - double radius_solar, - double luminosity_solar, - double ra_deg, - double dec_deg, - double epoch_jd, - const std::optional& pm = std::nullopt) { - SiderustStar* h = nullptr; - const siderust_proper_motion_t* pm_ptr = nullptr; - siderust_proper_motion_t pm_c{}; - if (pm.has_value()) { - pm_c = pm->to_c(); - pm_ptr = &pm_c; - } - check_status(siderust_star_create( - name.c_str(), distance_ly, mass_solar, radius_solar, - luminosity_solar, ra_deg, dec_deg, epoch_jd, pm_ptr, &h), - "Star::create"); - return Star(h); - } - - // -- Accessors -- - - std::string name() const { - char buf[256]; - uintptr_t written = 0; - check_status(siderust_star_name(m_handle, buf, sizeof(buf), &written), - "Star::name"); - return std::string(buf, written); - } - - double distance_ly() const { return siderust_star_distance_ly(m_handle); } - double mass_solar() const { return siderust_star_mass_solar(m_handle); } - double radius_solar() const { return siderust_star_radius_solar(m_handle); } - double luminosity_solar() const { return siderust_star_luminosity_solar(m_handle); } + check_status(siderust_star_create(name.c_str(), distance_ly, mass_solar, + radius_solar, luminosity_solar, ra_deg, + dec_deg, epoch_jd, pm_ptr, &h), + "Star::create"); + return Star(h); + } + + // -- Accessors -- + + std::string name() const { + char buf[256]; + uintptr_t written = 0; + check_status(siderust_star_name(m_handle, buf, sizeof(buf), &written), + "Star::name"); + return std::string(buf, written); + } + + double distance_ly() const { return siderust_star_distance_ly(m_handle); } + double mass_solar() const { return siderust_star_mass_solar(m_handle); } + double radius_solar() const { return siderust_star_radius_solar(m_handle); } + double luminosity_solar() const { + return siderust_star_luminosity_solar(m_handle); + } }; -inline const Star VEGA = Star::catalog("VEGA"); -inline const Star SIRIUS = Star::catalog("SIRIUS"); -inline const Star POLARIS = Star::catalog("POLARIS"); -inline const Star CANOPUS = Star::catalog("CANOPUS"); -inline const Star ARCTURUS = Star::catalog("ARCTURUS"); -inline const Star RIGEL = Star::catalog("RIGEL"); +inline const Star VEGA = Star::catalog("VEGA"); +inline const Star SIRIUS = Star::catalog("SIRIUS"); +inline const Star POLARIS = Star::catalog("POLARIS"); +inline const Star CANOPUS = Star::catalog("CANOPUS"); +inline const Star ARCTURUS = Star::catalog("ARCTURUS"); +inline const Star RIGEL = Star::catalog("RIGEL"); inline const Star BETELGEUSE = Star::catalog("BETELGEUSE"); -inline const Star PROCYON = Star::catalog("PROCYON"); -inline const Star ALDEBARAN = Star::catalog("ALDEBARAN"); -inline const Star ALTAIR = Star::catalog("ALTAIR"); +inline const Star PROCYON = Star::catalog("PROCYON"); +inline const Star ALDEBARAN = Star::catalog("ALDEBARAN"); +inline const Star ALTAIR = Star::catalog("ALTAIR"); } // namespace siderust diff --git a/include/siderust/body_target.hpp b/include/siderust/body_target.hpp index cc39e2a..855bafd 100644 --- a/include/siderust/body_target.hpp +++ b/include/siderust/body_target.hpp @@ -43,15 +43,15 @@ namespace siderust { * Maps 1:1 to the FFI `SiderustBody` discriminant. */ enum class Body : int32_t { - Sun = SIDERUST_BODY_SUN, - Moon = SIDERUST_BODY_MOON, - Mercury = SIDERUST_BODY_MERCURY, - Venus = SIDERUST_BODY_VENUS, - Mars = SIDERUST_BODY_MARS, - Jupiter = SIDERUST_BODY_JUPITER, - Saturn = SIDERUST_BODY_SATURN, - Uranus = SIDERUST_BODY_URANUS, - Neptune = SIDERUST_BODY_NEPTUNE, + Sun = SIDERUST_BODY_SUN, + Moon = SIDERUST_BODY_MOON, + Mercury = SIDERUST_BODY_MERCURY, + Venus = SIDERUST_BODY_VENUS, + Mars = SIDERUST_BODY_MARS, + Jupiter = SIDERUST_BODY_JUPITER, + Saturn = SIDERUST_BODY_SATURN, + Uranus = SIDERUST_BODY_URANUS, + Neptune = SIDERUST_BODY_NEPTUNE, }; // ============================================================================ @@ -63,92 +63,93 @@ namespace body { /** * @brief Compute a body's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(Body b, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_body_altitude_at( - static_cast(b), obs.to_c(), mjd.value(), &out), - "body::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(Body b, const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_body_altitude_at(static_cast(b), + obs.to_c(), mjd.value(), &out), + "body::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when a body is above a threshold altitude. */ -inline std::vector above_threshold( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_above_threshold( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_above_threshold( + static_cast(b), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "body::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Find periods when a body is below a threshold altitude. */ -inline std::vector below_threshold( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_below_threshold( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_below_threshold( + static_cast(b), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "body::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Find threshold-crossing events for a body. */ -inline std::vector crossings( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_crossings( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_crossings(static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Find culmination events for a body. */ -inline std::vector culminations( - Body b, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_culminations( - static_cast(b), obs.to_c(), - window.c_inner(), - opts.to_c(), &ptr, &count), - "body::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(Body b, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_culminations(static_cast(b), + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "body::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Find periods when a body's altitude is within [min, max]. */ -inline std::vector altitude_periods( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_altitude_periods( - static_cast(b), q, &ptr, &count), - "body::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_altitude_periods(static_cast(b), q, + &ptr, &count), + "body::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace body @@ -160,60 +161,58 @@ namespace body { /** * @brief Compute a body's azimuth (radians) at a given MJD instant. */ -inline qtty::Radian azimuth_at(Body b, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_body_azimuth_at( - static_cast(b), obs.to_c(), mjd.value(), &out), - "body::azimuth_at"); - return qtty::Radian(out); +inline qtty::Radian azimuth_at(Body b, const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_body_azimuth_at(static_cast(b), + obs.to_c(), mjd.value(), &out), + "body::azimuth_at"); + return qtty::Radian(out); } /** * @brief Find azimuth-bearing crossing events for a body. */ -inline std::vector azimuth_crossings( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_azimuth_crossings( - static_cast(b), obs.to_c(), - window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "body::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(Body b, const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_crossings( + static_cast(b), obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "body::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Find azimuth extrema (northernmost/southernmost bearing) for a body. */ -inline std::vector azimuth_extrema( - Body b, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_azimuth_extrema( - static_cast(b), obs.to_c(), - window.c_inner(), - opts.to_c(), &ptr, &count), - "body::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(Body b, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_extrema(static_cast(b), + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "body::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Find periods when a body's azimuth is within [min, max]. */ -inline std::vector in_azimuth_range( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree min, qtty::Degree max, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_in_azimuth_range( - static_cast(b), obs.to_c(), - window.c_inner(), min.value(), max.value(), - opts.to_c(), &ptr, &count), - "body::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree min, qtty::Degree max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_in_azimuth_range( + static_cast(b), obs.to_c(), window.c_inner(), + min.value(), max.value(), opts.to_c(), &ptr, &count), + "body::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } } // namespace body @@ -233,66 +232,69 @@ inline std::vector in_azimuth_range( * polymorphic dispatch. */ class BodyTarget : public Trackable { - public: - /** - * @brief Construct a BodyTarget for a given solar-system body. - * @param body The body to track. - */ - explicit BodyTarget(Body body) : body_(body) {} - - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ - - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - auto rad = body::altitude_at(body_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::above_threshold(body_, obs, window, threshold, opts); - } - - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::below_threshold(body_, obs, window, threshold, opts); - } - - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::crossings(body_, obs, window, threshold, opts); - } - - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - return body::culminations(body_, obs, window, opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ - - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - auto rad = body::azimuth_at(body_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - return body::azimuth_crossings(body_, obs, window, bearing, opts); - } - - /// Access the underlying Body enum value. - Body body() const { return body_; } - - private: - Body body_; +public: + /** + * @brief Construct a BodyTarget for a given solar-system body. + * @param body The body to track. + */ + explicit BodyTarget(Body body) : body_(body) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + auto rad = body::altitude_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::above_threshold(body_, obs, window, threshold, opts); + } + + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::below_threshold(body_, obs, window, threshold, opts); + } + + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::crossings(body_, obs, window, threshold, opts); + } + + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + return body::culminations(body_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + auto rad = body::azimuth_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + return body::azimuth_crossings(body_, obs, window, bearing, opts); + } + + /// Access the underlying Body enum value. + Body body() const { return body_; } + +private: + Body body_; }; } // namespace siderust diff --git a/include/siderust/centers.hpp b/include/siderust/centers.hpp index 0a67de4..6c0ada0 100644 --- a/include/siderust/centers.hpp +++ b/include/siderust/centers.hpp @@ -23,17 +23,15 @@ namespace centers { // Center Trait // ============================================================================ -template -struct CenterTraits; // primary — intentionally undefined +template struct CenterTraits; // primary — intentionally undefined -template -struct is_center : std::false_type {}; +template struct is_center : std::false_type {}; template -struct is_center::ffi_id)>> : std::true_type {}; +struct is_center::ffi_id)>> + : std::true_type {}; -template -inline constexpr bool is_center_v = is_center::value; +template inline constexpr bool is_center_v = is_center::value; // ============================================================================ // Center Tag Definitions @@ -57,39 +55,34 @@ struct Bodycentric {}; /// Marker for simple (no-parameter) centers. struct NoParams {}; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BARYCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Barycentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BARYCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Barycentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_HELIOCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Heliocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_HELIOCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Heliocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_GEOCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Geocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_GEOCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Geocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_TOPOCENTRIC; - using Params = Geodetic; // forward-declared - static constexpr const char* name() { return "Topocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_TOPOCENTRIC; + using Params = Geodetic; // forward-declared + static constexpr const char *name() { return "Topocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BODYCENTRIC; - using Params = NoParams; // placeholder for BodycentricParams - static constexpr const char* name() { return "Bodycentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BODYCENTRIC; + using Params = NoParams; // placeholder for BodycentricParams + static constexpr const char *name() { return "Bodycentric"; } }; // ============================================================================ @@ -105,14 +98,11 @@ struct CenterTraits { template struct has_center_transform : std::false_type {}; -template -struct has_center_transform : std::true_type {}; +template struct has_center_transform : std::true_type {}; -#define SIDERUST_CENTER_TRANSFORM_PAIR(A, B) \ - template <> \ - struct has_center_transform : std::true_type {}; \ - template <> \ - struct has_center_transform : std::true_type {} +#define SIDERUST_CENTER_TRANSFORM_PAIR(A, B) \ + template <> struct has_center_transform : std::true_type {}; \ + template <> struct has_center_transform : std::true_type {} SIDERUST_CENTER_TRANSFORM_PAIR(Barycentric, Heliocentric); SIDERUST_CENTER_TRANSFORM_PAIR(Barycentric, Geocentric); @@ -121,7 +111,8 @@ SIDERUST_CENTER_TRANSFORM_PAIR(Heliocentric, Geocentric); #undef SIDERUST_CENTER_TRANSFORM_PAIR template -inline constexpr bool has_center_transform_v = has_center_transform::value; +inline constexpr bool has_center_transform_v = + has_center_transform::value; } // namespace centers } // namespace siderust diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 13adad1..2a681b2 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -25,20 +25,19 @@ namespace cartesian { * @ingroup coordinates_cartesian * @tparam F Reference frame tag (e.g. `frames::ICRS`). */ -template -struct Direction { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); +template struct Direction { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); - double x; ///< X component (unitless). - double y; ///< Y component (unitless). - double z; ///< Z component (unitless). + double x; ///< X component (unitless). + double y; ///< Y component (unitless). + double z; ///< Z component (unitless). - Direction() : x(0), y(0), z(0) {} - Direction(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} + Direction() : x(0), y(0), z(0) {} + Direction(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} - static constexpr siderust_frame_t frame_id() { - return frames::FrameTraits::ffi_id; - } + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } }; /** @@ -51,41 +50,42 @@ struct Direction { * @tparam F Reference frame tag (e.g. `frames::ECEF`). * @tparam U Length unit (default: `qtty::Meter`). */ -template -struct Position { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - static_assert(centers::is_center_v, "C must be a valid center tag"); - - U comp_x; ///< X component. - U comp_y; ///< Y component. - U comp_z; ///< Z component. - - Position() - : comp_x(U(0)), comp_y(U(0)), comp_z(U(0)) {} - - Position(U x_, U y_, U z_) - : comp_x(x_), comp_y(y_), comp_z(z_) {} - - Position(double x_, double y_, double z_) - : comp_x(U(x_)), comp_y(U(y_)), comp_z(U(z_)) {} - - U x() const { return comp_x; } - U y() const { return comp_y; } - U z() const { return comp_z; } - - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } - - /// Convert to C FFI struct. - siderust_cartesian_pos_t to_c() const { - return {comp_x.value(), comp_y.value(), comp_z.value(), - frame_id(), center_id()}; - } - - /// Create from C FFI struct (ignoring runtime frame/center - trust the type). - static Position from_c(const siderust_cartesian_pos_t& c) { - return Position(c.x, c.y, c.z); - } +template struct Position { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + static_assert(centers::is_center_v, "C must be a valid center tag"); + + U comp_x; ///< X component. + U comp_y; ///< Y component. + U comp_z; ///< Z component. + + Position() : comp_x(U(0)), comp_y(U(0)), comp_z(U(0)) {} + + Position(U x_, U y_, U z_) : comp_x(x_), comp_y(y_), comp_z(z_) {} + + Position(double x_, double y_, double z_) + : comp_x(U(x_)), comp_y(U(y_)), comp_z(U(z_)) {} + + U x() const { return comp_x; } + U y() const { return comp_y; } + U z() const { return comp_z; } + + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr siderust_center_t center_id() { + return centers::CenterTraits::ffi_id; + } + + /// Convert to C FFI struct. + siderust_cartesian_pos_t to_c() const { + return {comp_x.value(), comp_y.value(), comp_z.value(), frame_id(), + center_id()}; + } + + /// Create from C FFI struct (ignoring runtime frame/center - trust the type). + static Position from_c(const siderust_cartesian_pos_t &c) { + return Position(c.x, c.y, c.z); + } }; // ============================================================================ @@ -96,8 +96,9 @@ struct Position { * @brief Stream operator for Position. */ template -inline std::ostream& operator<<(std::ostream& os, const Position& pos) { - return os << pos.x() << ", " << pos.y() << ", " << pos.z(); +inline std::ostream &operator<<(std::ostream &os, + const Position &pos) { + return os << pos.x() << ", " << pos.y() << ", " << pos.z(); } } // namespace cartesian diff --git a/include/siderust/coordinates/conversions.hpp b/include/siderust/coordinates/conversions.hpp index fe9b727..b3fd680 100644 --- a/include/siderust/coordinates/conversions.hpp +++ b/include/siderust/coordinates/conversions.hpp @@ -13,15 +13,13 @@ namespace siderust { template inline cartesian::Position Geodetic::to_cartesian() const { - siderust_cartesian_pos_t out; - check_status( - siderust_geodetic_to_cartesian_ecef(to_c(), &out), - "Geodetic::to_cartesian"); - const auto ecef_m = cartesian::position::ECEF::from_c(out); - return cartesian::Position( - ecef_m.x().template to(), - ecef_m.y().template to(), - ecef_m.z().template to()); + siderust_cartesian_pos_t out; + check_status(siderust_geodetic_to_cartesian_ecef(to_c(), &out), + "Geodetic::to_cartesian"); + const auto ecef_m = cartesian::position::ECEF::from_c(out); + return cartesian::Position( + ecef_m.x().template to(), ecef_m.y().template to(), + ecef_m.z().template to()); } /** @@ -29,8 +27,9 @@ Geodetic::to_cartesian() const { * * @ingroup coordinates_conversions */ -inline cartesian::position::ECEF geodetic_to_cartesian_ecef(const Geodetic& geo) { - return geo.to_cartesian(); +inline cartesian::position::ECEF +geodetic_to_cartesian_ecef(const Geodetic &geo) { + return geo.to_cartesian(); } } // namespace siderust diff --git a/include/siderust/coordinates/geodetic.hpp b/include/siderust/coordinates/geodetic.hpp index d244b13..32c5e69 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -16,8 +16,7 @@ namespace siderust { namespace cartesian { -template -struct Position; +template struct Position; } /** @@ -28,38 +27,39 @@ struct Position; * @ingroup coordinates_geodetic */ struct Geodetic { - qtty::Degree lon; ///< Longitude (east positive). - qtty::Degree lat; ///< Latitude (north positive). - qtty::Meter height; ///< Height above ellipsoid. + qtty::Degree lon; ///< Longitude (east positive). + qtty::Degree lat; ///< Latitude (north positive). + qtty::Meter height; ///< Height above ellipsoid. - Geodetic() - : lon(qtty::Degree(0)), lat(qtty::Degree(0)), height(qtty::Meter(0)) {} + Geodetic() + : lon(qtty::Degree(0)), lat(qtty::Degree(0)), height(qtty::Meter(0)) {} - Geodetic(qtty::Degree lon_, qtty::Degree lat_, qtty::Meter h = qtty::Meter(0)) - : lon(lon_), lat(lat_), height(h) {} + Geodetic(qtty::Degree lon_, qtty::Degree lat_, qtty::Meter h = qtty::Meter(0)) + : lon(lon_), lat(lat_), height(h) {} - /// Raw-double convenience constructor (degrees, metres). - Geodetic(double lon_deg, double lat_deg, double height_m = 0.0) - : lon(qtty::Degree(lon_deg)), lat(qtty::Degree(lat_deg)), - height(qtty::Meter(height_m)) {} + /// Raw-double convenience constructor (degrees, metres). + Geodetic(double lon_deg, double lat_deg, double height_m = 0.0) + : lon(qtty::Degree(lon_deg)), lat(qtty::Degree(lat_deg)), + height(qtty::Meter(height_m)) {} - /// Convert to C FFI struct. - siderust_geodetic_t to_c() const { - return {lon.value(), lat.value(), height.value()}; - } + /// Convert to C FFI struct. + siderust_geodetic_t to_c() const { + return {lon.value(), lat.value(), height.value()}; + } - /// Create from C FFI struct. - static Geodetic from_c(const siderust_geodetic_t& c) { - return Geodetic(c.lon_deg, c.lat_deg, c.height_m); - } + /// Create from C FFI struct. + static Geodetic from_c(const siderust_geodetic_t &c) { + return Geodetic(c.lon_deg, c.lat_deg, c.height_m); + } - /** - * @brief Convert geodetic (WGS84/ECEF) to cartesian position. - * - * @tparam U Output length unit (default: meter). - */ - template - cartesian::Position to_cartesian() const; + /** + * @brief Convert geodetic (WGS84/ECEF) to cartesian position. + * + * @tparam U Output length unit (default: meter). + */ + template + cartesian::Position + to_cartesian() const; }; // ============================================================================ @@ -69,8 +69,8 @@ struct Geodetic { /** * @brief Stream operator for Geodetic. */ -inline std::ostream& operator<<(std::ostream& os, const Geodetic& geo) { - return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; +inline std::ostream &operator<<(std::ostream &os, const Geodetic &geo) { + return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; } } // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 56db30d..1053eb7 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -25,8 +25,9 @@ namespace spherical { * Mirrors Rust's `affn::spherical::Direction`. * * @ingroup coordinates_spherical - * @tparam F Reference frame chapter content removed. Restore the original from \texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. -tag (e.g. `frames::ICRS`). + * @tparam F Reference frame chapter content removed. Restore the original from +\texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. tag +(e.g. `frames::ICRS`). * * @par Accessors * Access values through frame-appropriate getters: @@ -34,134 +35,157 @@ tag (e.g. `frames::ICRS`). * - Horizontal frame: `az()`, `al()` / `alt()` * - Lon/lat frames: `lon()`, `lat()` */ -template -struct Direction { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - - private: - qtty::Degree azimuth_; ///< Azimuthal component (RA/longitude/azimuth). - qtty::Degree polar_; ///< Polar component (Dec/latitude/altitude). - - public: - Direction() : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)) {} - - Direction(qtty::Degree azimuth, qtty::Degree polar) - : azimuth_(azimuth), polar_(polar) {} - - /// @name Frame info - /// @{ - static constexpr siderust_frame_t frame_id() { - return frames::FrameTraits::ffi_id; - } - static constexpr const char* frame_name() { - return frames::FrameTraits::name(); - } - /// @} - - /// @name RA / Dec (equatorial frames only) - /// @{ - template , int> = 0> - qtty::Degree ra() const { return azimuth_; } - - template , int> = 0> - qtty::Degree dec() const { return polar_; } - /// @} - - /// @name Azimuth / Altitude (Horizontal frame only) - /// @{ - template , int> = 0> - qtty::Degree az() const { return azimuth_; } - - template , int> = 0> - qtty::Degree al() const { return polar_; } - - template , int> = 0> - qtty::Degree alt() const { return polar_; } - - template , int> = 0> - qtty::Degree altitude() const { return polar_; } - /// @} - - /// @name Longitude / Latitude (lon/lat frames) - /// @{ - template , int> = 0> - qtty::Degree lon() const { return azimuth_; } - - template , int> = 0> - qtty::Degree lat() const { return polar_; } - - template , int> = 0> - qtty::Degree longitude() const { return azimuth_; } - - template , int> = 0> - qtty::Degree latitude() const { return polar_; } - /// @} - - /// @name FFI interop - /// @{ - siderust_spherical_dir_t to_c() const { - return {polar_.value(), azimuth_.value(), frame_id()}; - } - - static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); - } - /// @} - - /** - * @brief Transform to a different reference frame. - * - * Only enabled for frame pairs with a FrameRotationProvider in the FFI. - * Attempting an unsupported transform is a compile-time error. - * - * @tparam Target Destination frame tag. - */ - template - std::enable_if_t< - frames::has_frame_transform_v, - Direction> - to_frame(const JulianDate& jd) const { - if constexpr (std::is_same_v) { - return Direction(azimuth_, polar_); - } else { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_transform_frame( - polar_.value(), azimuth_.value(), - frames::FrameTraits::ffi_id, - frames::FrameTraits::ffi_id, - jd.value(), &out), - "Direction::to_frame"); - return Direction::from_c(out); - } - } - - /** - * @brief Shorthand: `.to(jd)` (calls `to_frame`). - */ - template - auto to(const JulianDate& jd) const - -> decltype(this->template to_frame(jd)) { - return to_frame(jd); - } - - /** - * @brief Transform to the horizontal (alt-az) frame. - */ - template - std::enable_if_t< - frames::has_horizontal_transform_v, - Direction> - to_horizontal(const JulianDate& jd, const Geodetic& observer) const { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_to_horizontal( - polar_.value(), azimuth_.value(), - frames::FrameTraits::ffi_id, - jd.value(), observer.to_c(), &out), - "Direction::to_horizontal"); - return Direction::from_c(out); +template struct Direction { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + +private: + qtty::Degree azimuth_; ///< Azimuthal component (RA/longitude/azimuth). + qtty::Degree polar_; ///< Polar component (Dec/latitude/altitude). + +public: + Direction() : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)) {} + + Direction(qtty::Degree azimuth, qtty::Degree polar) + : azimuth_(azimuth), polar_(polar) {} + + /// @name Frame info + /// @{ + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr const char *frame_name() { + return frames::FrameTraits::name(); + } + /// @} + + /// @name RA / Dec (equatorial frames only) + /// @{ + template , int> = 0> + qtty::Degree ra() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree dec() const { + return polar_; + } + /// @} + + /// @name Azimuth / Altitude (Horizontal frame only) + /// @{ + template , int> = 0> + qtty::Degree az() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree al() const { + return polar_; + } + + template , int> = 0> + qtty::Degree alt() const { + return polar_; + } + + template , int> = 0> + qtty::Degree altitude() const { + return polar_; + } + /// @} + + /// @name Longitude / Latitude (lon/lat frames) + /// @{ + template , int> = 0> + qtty::Degree lon() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree lat() const { + return polar_; + } + + template , int> = 0> + qtty::Degree longitude() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree latitude() const { + return polar_; + } + /// @} + + /// @name FFI interop + /// @{ + siderust_spherical_dir_t to_c() const { + return {polar_.value(), azimuth_.value(), frame_id()}; + } + + static Direction from_c(const siderust_spherical_dir_t &c) { + return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); + } + /// @} + + /** + * @brief Transform to a different reference frame. + * + * Only enabled for frame pairs with a FrameRotationProvider in the FFI. + * Attempting an unsupported transform is a compile-time error. + * + * @tparam Target Destination frame tag. + */ + template + std::enable_if_t, Direction> + to_frame(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return Direction(azimuth_, polar_); + } else { + siderust_spherical_dir_t out; + check_status(siderust_spherical_dir_transform_frame( + polar_.value(), azimuth_.value(), + frames::FrameTraits::ffi_id, + frames::FrameTraits::ffi_id, jd.value(), &out), + "Direction::to_frame"); + return Direction::from_c(out); } + } + + /** + * @brief Shorthand: `.to(jd)` (calls `to_frame`). + */ + template + auto to(const JulianDate &jd) const + -> decltype(this->template to_frame(jd)) { + return to_frame(jd); + } + + /** + * @brief Transform to the horizontal (alt-az) frame. + */ + template + std::enable_if_t, + Direction> + to_horizontal(const JulianDate &jd, const Geodetic &observer) const { + siderust_spherical_dir_t out; + check_status( + siderust_spherical_dir_to_horizontal(polar_.value(), azimuth_.value(), + frames::FrameTraits::ffi_id, + jd.value(), observer.to_c(), &out), + "Direction::to_horizontal"); + return Direction::from_c(out); + } }; /** @@ -174,56 +198,78 @@ struct Direction { * @tparam F Reference frame tag (e.g. `frames::ICRS`). * @tparam U Distance unit (default: `qtty::Meter`). */ -template -struct Position { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - static_assert(centers::is_center_v, "C must be a valid center tag"); - - private: - qtty::Degree azimuth_; - qtty::Degree polar_; - U dist_; - - public: - Position() - : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)), dist_(U(0)) {} - - Position(qtty::Degree azimuth, qtty::Degree polar, U dist) - : azimuth_(azimuth), polar_(polar), dist_(dist) {} - - /// Extract the direction component. - Direction direction() const { - return Direction(azimuth_, polar_); - } - - /// @name Component accessors by frame convention - /// @{ - template , int> = 0> - qtty::Degree ra() const { return azimuth_; } - - template , int> = 0> - qtty::Degree dec() const { return polar_; } - - template , int> = 0> - qtty::Degree az() const { return azimuth_; } - - template , int> = 0> - qtty::Degree al() const { return polar_; } - - template , int> = 0> - qtty::Degree alt() const { return polar_; } - - template , int> = 0> - qtty::Degree lon() const { return azimuth_; } - - template , int> = 0> - qtty::Degree lat() const { return polar_; } - /// @} - - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } - - U distance() const { return dist_; } +template struct Position { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + static_assert(centers::is_center_v, "C must be a valid center tag"); + +private: + qtty::Degree azimuth_; + qtty::Degree polar_; + U dist_; + +public: + Position() + : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)), dist_(U(0)) {} + + Position(qtty::Degree azimuth, qtty::Degree polar, U dist) + : azimuth_(azimuth), polar_(polar), dist_(dist) {} + + /// Extract the direction component. + Direction direction() const { return Direction(azimuth_, polar_); } + + /// @name Component accessors by frame convention + /// @{ + template , int> = 0> + qtty::Degree ra() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree dec() const { + return polar_; + } + + template , int> = 0> + qtty::Degree az() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree al() const { + return polar_; + } + + template , int> = 0> + qtty::Degree alt() const { + return polar_; + } + + template , int> = 0> + qtty::Degree lon() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree lat() const { + return polar_; + } + /// @} + + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr siderust_center_t center_id() { + return centers::CenterTraits::ffi_id; + } + + U distance() const { return dist_; } }; // ============================================================================ @@ -234,24 +280,24 @@ struct Position { * @brief Stream operator for Direction with RA/Dec frames. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.ra() << ", " << dir.dec(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.ra() << ", " << dir.dec(); } /** * @brief Stream operator for Direction with Az/Alt frame. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.az() << ", " << dir.alt(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.az() << ", " << dir.alt(); } /** * @brief Stream operator for Direction with Lon/Lat frames. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.lon() << ", " << dir.lat(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.lon() << ", " << dir.lat(); } } // namespace spherical diff --git a/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp b/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp index d06e3f6..cd440a5 100644 --- a/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp +++ b/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp @@ -6,16 +6,20 @@ namespace siderust { namespace cartesian { namespace position { template -using EclipticMeanJ2000 = Position; +using EclipticMeanJ2000 = + Position; template -using HelioBarycentric = Position; +using HelioBarycentric = + Position; template -using GeoBarycentric = Position; +using GeoBarycentric = + Position; template -using MoonGeocentric = Position; +using MoonGeocentric = + Position; } // namespace position } // namespace cartesian } // namespace siderust diff --git a/include/siderust/coordinates/types/spherical/direction/equatorial.hpp b/include/siderust/coordinates/types/spherical/direction/equatorial.hpp index c920904..844a12e 100644 --- a/include/siderust/coordinates/types/spherical/direction/equatorial.hpp +++ b/include/siderust/coordinates/types/spherical/direction/equatorial.hpp @@ -5,9 +5,9 @@ namespace siderust { namespace spherical { namespace direction { -using ICRS = Direction; -using ICRF = Direction; -using EquatorialMeanJ2000 = Direction; +using ICRS = Direction; +using ICRF = Direction; +using EquatorialMeanJ2000 = Direction; using EquatorialMeanOfDate = Direction; using EquatorialTrueOfDate = Direction; } // namespace direction diff --git a/include/siderust/coordinates/types/spherical/position/ecliptic.hpp b/include/siderust/coordinates/types/spherical/position/ecliptic.hpp index ecf112b..8229992 100644 --- a/include/siderust/coordinates/types/spherical/position/ecliptic.hpp +++ b/include/siderust/coordinates/types/spherical/position/ecliptic.hpp @@ -6,7 +6,8 @@ namespace siderust { namespace spherical { namespace position { template -using EclipticMeanJ2000 = Position; +using EclipticMeanJ2000 = + Position; } // namespace position } // namespace spherical } // namespace siderust diff --git a/include/siderust/ephemeris.hpp b/include/siderust/ephemeris.hpp index d5377cb..59b715c 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -23,41 +23,48 @@ namespace ephemeris { /** * @brief Sun's barycentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::HelioBarycentric sun_barycentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_sun_barycentric(jd.value(), &out), - "ephemeris::sun_barycentric"); - return cartesian::position::HelioBarycentric::from_c(out); +inline cartesian::position::HelioBarycentric +sun_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_sun_barycentric(jd.value(), &out), + "ephemeris::sun_barycentric"); + return cartesian::position::HelioBarycentric::from_c( + out); } /** * @brief Earth's barycentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::GeoBarycentric earth_barycentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_earth_barycentric(jd.value(), &out), - "ephemeris::earth_barycentric"); - return cartesian::position::GeoBarycentric::from_c(out); +inline cartesian::position::GeoBarycentric +earth_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_earth_barycentric(jd.value(), &out), + "ephemeris::earth_barycentric"); + return cartesian::position::GeoBarycentric::from_c( + out); } /** * @brief Earth's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::EclipticMeanJ2000 earth_heliocentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_earth_heliocentric(jd.value(), &out), - "ephemeris::earth_heliocentric"); - return cartesian::position::EclipticMeanJ2000::from_c(out); +inline cartesian::position::EclipticMeanJ2000 +earth_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_earth_heliocentric(jd.value(), &out), + "ephemeris::earth_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c( + out); } /** * @brief Moon's geocentric position (EclipticMeanJ2000, km) via ELP2000. */ -inline cartesian::position::MoonGeocentric moon_geocentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_moon_geocentric(jd.value(), &out), - "ephemeris::moon_geocentric"); - return cartesian::position::MoonGeocentric::from_c(out); +inline cartesian::position::MoonGeocentric +moon_geocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_moon_geocentric(jd.value(), &out), + "ephemeris::moon_geocentric"); + return cartesian::position::MoonGeocentric::from_c(out); } } // namespace ephemeris diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index 71784ce..df3c5b2 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -26,91 +26,99 @@ namespace siderust { // ============================================================================ class SiderustException : public std::runtime_error { - public: - explicit SiderustException(const std::string& msg) : std::runtime_error(msg) {} +public: + explicit SiderustException(const std::string &msg) + : std::runtime_error(msg) {} }; class NullPointerError : public SiderustException { - public: - explicit NullPointerError(const std::string& msg) : SiderustException(msg) {} +public: + explicit NullPointerError(const std::string &msg) : SiderustException(msg) {} }; class InvalidFrameError : public SiderustException { - public: - explicit InvalidFrameError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidFrameError(const std::string &msg) : SiderustException(msg) {} }; class InvalidCenterError : public SiderustException { - public: - explicit InvalidCenterError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidCenterError(const std::string &msg) + : SiderustException(msg) {} }; class TransformFailedError : public SiderustException { - public: - explicit TransformFailedError(const std::string& msg) : SiderustException(msg) {} +public: + explicit TransformFailedError(const std::string &msg) + : SiderustException(msg) {} }; class InvalidBodyError : public SiderustException { - public: - explicit InvalidBodyError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidBodyError(const std::string &msg) : SiderustException(msg) {} }; class UnknownStarError : public SiderustException { - public: - explicit UnknownStarError(const std::string& msg) : SiderustException(msg) {} +public: + explicit UnknownStarError(const std::string &msg) : SiderustException(msg) {} }; class InvalidPeriodError : public SiderustException { - public: - explicit InvalidPeriodError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidPeriodError(const std::string &msg) + : SiderustException(msg) {} }; class AllocationFailedError : public SiderustException { - public: - explicit AllocationFailedError(const std::string& msg) : SiderustException(msg) {} +public: + explicit AllocationFailedError(const std::string &msg) + : SiderustException(msg) {} }; class InvalidArgumentError : public SiderustException { - public: - explicit InvalidArgumentError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidArgumentError(const std::string &msg) + : SiderustException(msg) {} }; // ============================================================================ // Error Translation // ============================================================================ -inline void check_status(siderust_status_t status, const char* operation) { - if (status == SIDERUST_STATUS_T_OK) - return; - - std::string msg = std::string(operation) + " failed: "; - switch (status) { - case SIDERUST_STATUS_T_NULL_POINTER: - throw NullPointerError(msg + "null output pointer"); - case SIDERUST_STATUS_T_INVALID_FRAME: - throw InvalidFrameError(msg + "invalid or unsupported frame"); - case SIDERUST_STATUS_T_INVALID_CENTER: - throw InvalidCenterError(msg + "invalid or unsupported center"); - case SIDERUST_STATUS_T_TRANSFORM_FAILED: - throw TransformFailedError(msg + "coordinate transform failed"); - case SIDERUST_STATUS_T_INVALID_BODY: - throw InvalidBodyError(msg + "invalid body"); - case SIDERUST_STATUS_T_UNKNOWN_STAR: - throw UnknownStarError(msg + "unknown star name"); - case SIDERUST_STATUS_T_INVALID_PERIOD: - throw InvalidPeriodError(msg + "invalid period (start > end)"); - case SIDERUST_STATUS_T_ALLOCATION_FAILED: - throw AllocationFailedError(msg + "memory allocation failed"); - case SIDERUST_STATUS_T_INVALID_ARGUMENT: - throw InvalidArgumentError(msg + "invalid argument"); - default: - throw SiderustException(msg + "unknown error (" + std::to_string(status) + ")"); - } +inline void check_status(siderust_status_t status, const char *operation) { + if (status == SIDERUST_STATUS_T_OK) + return; + + std::string msg = std::string(operation) + " failed: "; + switch (status) { + case SIDERUST_STATUS_T_NULL_POINTER: + throw NullPointerError(msg + "null output pointer"); + case SIDERUST_STATUS_T_INVALID_FRAME: + throw InvalidFrameError(msg + "invalid or unsupported frame"); + case SIDERUST_STATUS_T_INVALID_CENTER: + throw InvalidCenterError(msg + "invalid or unsupported center"); + case SIDERUST_STATUS_T_TRANSFORM_FAILED: + throw TransformFailedError(msg + "coordinate transform failed"); + case SIDERUST_STATUS_T_INVALID_BODY: + throw InvalidBodyError(msg + "invalid body"); + case SIDERUST_STATUS_T_UNKNOWN_STAR: + throw UnknownStarError(msg + "unknown star name"); + case SIDERUST_STATUS_T_INVALID_PERIOD: + throw InvalidPeriodError(msg + "invalid period (start > end)"); + case SIDERUST_STATUS_T_ALLOCATION_FAILED: + throw AllocationFailedError(msg + "memory allocation failed"); + case SIDERUST_STATUS_T_INVALID_ARGUMENT: + throw InvalidArgumentError(msg + "invalid argument"); + default: + throw SiderustException(msg + "unknown error (" + std::to_string(status) + + ")"); + } } /// @brief Backward-compatible wrapper — delegates to tempoch::check_status. -inline void check_tempoch_status(tempoch_status_t status, const char* operation) { - tempoch::check_status(status, operation); +inline void check_tempoch_status(tempoch_status_t status, + const char *operation) { + tempoch::check_status(status, operation); } // ============================================================================ @@ -118,79 +126,78 @@ inline void check_tempoch_status(tempoch_status_t status, const char* operation) // ============================================================================ /** - * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + patch). + * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + + * patch). */ -inline uint32_t ffi_version() { - return siderust_ffi_version(); -} +inline uint32_t ffi_version() { return siderust_ffi_version(); } // ============================================================================ // Frame and Center Enums (C++ typed) // ============================================================================ enum class Frame : int32_t { - ICRS = SIDERUST_FRAME_T_ICRS, - EclipticMeanJ2000 = SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, - EquatorialMeanJ2000 = SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, - EquatorialMeanOfDate = SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, - EquatorialTrueOfDate = SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, - Horizontal = SIDERUST_FRAME_T_HORIZONTAL, - ECEF = SIDERUST_FRAME_T_ECEF, - Galactic = SIDERUST_FRAME_T_GALACTIC, - GCRS = SIDERUST_FRAME_T_GCRS, - EclipticOfDate = SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, - EclipticTrueOfDate = SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, - CIRS = SIDERUST_FRAME_T_CIRS, - TIRS = SIDERUST_FRAME_T_TIRS, - ITRF = SIDERUST_FRAME_T_ITRF, - ICRF = SIDERUST_FRAME_T_ICRF, + ICRS = SIDERUST_FRAME_T_ICRS, + EclipticMeanJ2000 = SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + EquatorialMeanJ2000 = SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, + EquatorialMeanOfDate = SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, + EquatorialTrueOfDate = SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, + Horizontal = SIDERUST_FRAME_T_HORIZONTAL, + ECEF = SIDERUST_FRAME_T_ECEF, + Galactic = SIDERUST_FRAME_T_GALACTIC, + GCRS = SIDERUST_FRAME_T_GCRS, + EclipticOfDate = SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, + EclipticTrueOfDate = SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, + CIRS = SIDERUST_FRAME_T_CIRS, + TIRS = SIDERUST_FRAME_T_TIRS, + ITRF = SIDERUST_FRAME_T_ITRF, + ICRF = SIDERUST_FRAME_T_ICRF, }; enum class Center : int32_t { - Barycentric = SIDERUST_CENTER_T_BARYCENTRIC, - Heliocentric = SIDERUST_CENTER_T_HELIOCENTRIC, - Geocentric = SIDERUST_CENTER_T_GEOCENTRIC, - Topocentric = SIDERUST_CENTER_T_TOPOCENTRIC, - Bodycentric = SIDERUST_CENTER_T_BODYCENTRIC, + Barycentric = SIDERUST_CENTER_T_BARYCENTRIC, + Heliocentric = SIDERUST_CENTER_T_HELIOCENTRIC, + Geocentric = SIDERUST_CENTER_T_GEOCENTRIC, + Topocentric = SIDERUST_CENTER_T_TOPOCENTRIC, + Bodycentric = SIDERUST_CENTER_T_BODYCENTRIC, }; enum class CrossingDirection : int32_t { - Rising = SIDERUST_CROSSING_DIRECTION_T_RISING, - Setting = SIDERUST_CROSSING_DIRECTION_T_SETTING, + Rising = SIDERUST_CROSSING_DIRECTION_T_RISING, + Setting = SIDERUST_CROSSING_DIRECTION_T_SETTING, }; enum class CulminationKind : int32_t { - Max = SIDERUST_CULMINATION_KIND_T_MAX, - Min = SIDERUST_CULMINATION_KIND_T_MIN, + Max = SIDERUST_CULMINATION_KIND_T_MAX, + Min = SIDERUST_CULMINATION_KIND_T_MIN, }; // ============================================================================ // Stream operators for enums // ============================================================================ -inline std::ostream& operator<<(std::ostream& os, CrossingDirection dir) { - switch (dir) { - case CrossingDirection::Rising: - return os << "rising"; - case CrossingDirection::Setting: - return os << "setting"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, CrossingDirection dir) { + switch (dir) { + case CrossingDirection::Rising: + return os << "rising"; + case CrossingDirection::Setting: + return os << "setting"; + } + return os << "unknown"; } -inline std::ostream& operator<<(std::ostream& os, CulminationKind kind) { - switch (kind) { - case CulminationKind::Max: - return os << "max"; - case CulminationKind::Min: - return os << "min"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, CulminationKind kind) { + switch (kind) { + case CulminationKind::Max: + return os << "max"; + case CulminationKind::Min: + return os << "min"; + } + return os << "unknown"; } enum class RaConvention : int32_t { - MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, - MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, + MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, + MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, }; } // namespace siderust diff --git a/include/siderust/frames.hpp b/include/siderust/frames.hpp index 2e2b0f3..fe3bb5c 100644 --- a/include/siderust/frames.hpp +++ b/include/siderust/frames.hpp @@ -30,14 +30,13 @@ struct FrameTraits; // primary template — intentionally undefined /** * @brief Concept-like compile-time check (C++17: constexpr bool). */ -template -struct is_frame : std::false_type {}; +template struct is_frame : std::false_type {}; template -struct is_frame::ffi_id)>> : std::true_type {}; +struct is_frame::ffi_id)>> + : std::true_type {}; -template -inline constexpr bool is_frame_v = is_frame::value; +template inline constexpr bool is_frame_v = is_frame::value; // ============================================================================ // Frame Tag Definitions @@ -80,21 +79,30 @@ struct EclipticMeanOfDate {}; // FrameTraits Specializations // ============================================================================ -#define SIDERUST_DEFINE_FRAME(Tag, EnumVal, Label) \ - template <> \ - struct FrameTraits { \ - static constexpr siderust_frame_t ffi_id = EnumVal; \ - static constexpr const char* name() { return Label; } \ - } +#define SIDERUST_DEFINE_FRAME(Tag, EnumVal, Label) \ + template <> struct FrameTraits { \ + static constexpr siderust_frame_t ffi_id = EnumVal; \ + static constexpr const char *name() { return Label; } \ + } SIDERUST_DEFINE_FRAME(ICRS, SIDERUST_FRAME_T_ICRS, "ICRS"); SIDERUST_DEFINE_FRAME(ICRF, SIDERUST_FRAME_T_ICRF, "ICRF"); -SIDERUST_DEFINE_FRAME(EclipticMeanJ2000, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, "EclipticMeanJ2000"); -SIDERUST_DEFINE_FRAME(EclipticOfDate, SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, "EclipticOfDate"); -SIDERUST_DEFINE_FRAME(EclipticTrueOfDate, SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, "EclipticTrueOfDate"); -SIDERUST_DEFINE_FRAME(EquatorialMeanJ2000, SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, "EquatorialMeanJ2000"); -SIDERUST_DEFINE_FRAME(EquatorialMeanOfDate, SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, "EquatorialMeanOfDate"); -SIDERUST_DEFINE_FRAME(EquatorialTrueOfDate, SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, "EquatorialTrueOfDate"); +SIDERUST_DEFINE_FRAME(EclipticMeanJ2000, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + "EclipticMeanJ2000"); +SIDERUST_DEFINE_FRAME(EclipticOfDate, SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, + "EclipticOfDate"); +SIDERUST_DEFINE_FRAME(EclipticTrueOfDate, + SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, + "EclipticTrueOfDate"); +SIDERUST_DEFINE_FRAME(EquatorialMeanJ2000, + SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, + "EquatorialMeanJ2000"); +SIDERUST_DEFINE_FRAME(EquatorialMeanOfDate, + SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, + "EquatorialMeanOfDate"); +SIDERUST_DEFINE_FRAME(EquatorialTrueOfDate, + SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, + "EquatorialTrueOfDate"); SIDERUST_DEFINE_FRAME(Horizontal, SIDERUST_FRAME_T_HORIZONTAL, "Horizontal"); SIDERUST_DEFINE_FRAME(Galactic, SIDERUST_FRAME_T_GALACTIC, "Galactic"); SIDERUST_DEFINE_FRAME(ECEF, SIDERUST_FRAME_T_ECEF, "ECEF"); @@ -112,60 +120,52 @@ SIDERUST_DEFINE_FRAME(TIRS, SIDERUST_FRAME_T_TIRS, "TIRS"); /** * @brief Maps a frame to its conventional spherical-coordinate names. * - * Default: (longitude, latitude). Specialise per-frame for RA/Dec, Az/Alt, etc. + * Default: (longitude, latitude). Specialise per-frame for RA/Dec, Az/Alt, + * etc. */ -template -struct SphericalNaming { - static constexpr const char* lon_name() { return "longitude"; } - static constexpr const char* lat_name() { return "latitude"; } +template struct SphericalNaming { + static constexpr const char *lon_name() { return "longitude"; } + static constexpr const char *lat_name() { return "latitude"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "azimuth"; } - static constexpr const char* lat_name() { return "altitude"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "azimuth"; } + static constexpr const char *lat_name() { return "altitude"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "l"; } - static constexpr const char* lat_name() { return "b"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "l"; } + static constexpr const char *lat_name() { return "b"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "ecliptic_longitude"; } - static constexpr const char* lat_name() { return "ecliptic_latitude"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "ecliptic_longitude"; } + static constexpr const char *lat_name() { return "ecliptic_latitude"; } }; // ============================================================================ @@ -177,58 +177,38 @@ struct SphericalNaming { * * Use `has_ra_dec_v` in `std::enable_if_t` to gate RA/Dec accessors. */ -template -struct has_ra_dec : std::false_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template -inline constexpr bool has_ra_dec_v = has_ra_dec::value; +template struct has_ra_dec : std::false_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template inline constexpr bool has_ra_dec_v = has_ra_dec::value; /** * @brief True for the horizontal frame that exposes azimuth / altitude. * * Use `has_az_alt_v` to gate Az/Alt accessors. */ -template -struct has_az_alt : std::false_type {}; -template <> -struct has_az_alt : std::true_type {}; -template -inline constexpr bool has_az_alt_v = has_az_alt::value; +template struct has_az_alt : std::false_type {}; +template <> struct has_az_alt : std::true_type {}; +template inline constexpr bool has_az_alt_v = has_az_alt::value; /** * @brief True for ecliptic and galactic frames that use longitude / latitude. * * Use `has_lon_lat_v` to gate lon/lat accessors. */ -template -struct has_lon_lat : std::false_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; +template struct has_lon_lat : std::false_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; template inline constexpr bool has_lon_lat_v = has_lon_lat::value; @@ -250,15 +230,12 @@ template struct has_frame_transform : std::false_type {}; // Identity -template -struct has_frame_transform : std::true_type {}; +template struct has_frame_transform : std::true_type {}; // Hub spokes (bidirectional) -#define SIDERUST_FRAME_TRANSFORM_PAIR(A, B) \ - template <> \ - struct has_frame_transform : std::true_type {}; \ - template <> \ - struct has_frame_transform : std::true_type {} +#define SIDERUST_FRAME_TRANSFORM_PAIR(A, B) \ + template <> struct has_frame_transform : std::true_type {}; \ + template <> struct has_frame_transform : std::true_type {} // All pairs reachable through the ICRS hub SIDERUST_FRAME_TRANSFORM_PAIR(ICRS, EclipticMeanJ2000); @@ -281,18 +258,16 @@ SIDERUST_FRAME_TRANSFORM_PAIR(ICRF, ICRS); #undef SIDERUST_FRAME_TRANSFORM_PAIR template -inline constexpr bool has_frame_transform_v = has_frame_transform::value; +inline constexpr bool has_frame_transform_v = + has_frame_transform::value; /** * @brief Marks frames from which to_horizontal is reachable. */ -template -struct has_horizontal_transform : std::false_type {}; +template struct has_horizontal_transform : std::false_type {}; -template <> -struct has_horizontal_transform : std::true_type {}; -template <> -struct has_horizontal_transform : std::true_type {}; +template <> struct has_horizontal_transform : std::true_type {}; +template <> struct has_horizontal_transform : std::true_type {}; template <> struct has_horizontal_transform : std::true_type {}; template <> @@ -303,7 +278,8 @@ template <> struct has_horizontal_transform : std::true_type {}; template -inline constexpr bool has_horizontal_transform_v = has_horizontal_transform::value; +inline constexpr bool has_horizontal_transform_v = + has_horizontal_transform::value; } // namespace frames } // namespace siderust diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index 58704ca..1db1aa6 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -28,24 +28,24 @@ namespace siderust { * @brief Principal lunar phase kinds (new-moon quarter events). */ enum class PhaseKind : int32_t { - NewMoon = 0, - FirstQuarter = 1, - FullMoon = 2, - LastQuarter = 3, + NewMoon = 0, + FirstQuarter = 1, + FullMoon = 2, + LastQuarter = 3, }; /** * @brief Descriptive moon phase labels (8 canonical phases). */ enum class MoonPhaseLabel : int32_t { - NewMoon = 0, - WaxingCrescent = 1, - FirstQuarter = 2, - WaxingGibbous = 3, - FullMoon = 4, - WaningGibbous = 5, - LastQuarter = 6, - WaningCrescent = 7, + NewMoon = 0, + WaxingCrescent = 1, + FirstQuarter = 2, + WaxingGibbous = 3, + FullMoon = 4, + WaningGibbous = 5, + LastQuarter = 6, + WaningCrescent = 7, }; // ============================================================================ @@ -56,27 +56,27 @@ enum class MoonPhaseLabel : int32_t { * @brief Geometric description of the Moon's phase at a point in time. */ struct MoonPhaseGeometry { - double phase_angle_rad; ///< Phase angle in [0, π], radians. - double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. - double elongation_rad; ///< Sun–Moon elongation, radians. - bool waxing; ///< True when the Moon is waxing. - - static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t& c) { - return {c.phase_angle_rad, c.illuminated_fraction, - c.elongation_rad, static_cast(c.waxing)}; - } + double phase_angle_rad; ///< Phase angle in [0, π], radians. + double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. + double elongation_rad; ///< Sun–Moon elongation, radians. + bool waxing; ///< True when the Moon is waxing. + + static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t &c) { + return {c.phase_angle_rad, c.illuminated_fraction, c.elongation_rad, + static_cast(c.waxing)}; + } }; /** * @brief A principal lunar phase event (new moon, first quarter, etc.). */ struct PhaseEvent { - MJD time; ///< Epoch of the event (MJD). - PhaseKind kind; ///< Which principal phase occurred. + MJD time; ///< Epoch of the event (MJD). + PhaseKind kind; ///< Which principal phase occurred. - static PhaseEvent from_c(const siderust_phase_event_t& c) { - return {MJD(c.mjd), static_cast(c.kind)}; - } + static PhaseEvent from_c(const siderust_phase_event_t &c) { + return {MJD(c.mjd), static_cast(c.kind)}; + } }; // ============================================================================ @@ -84,28 +84,28 @@ struct PhaseEvent { // ============================================================================ namespace detail { -inline std::vector phase_events_from_c( - siderust_phase_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(PhaseEvent::from_c(ptr[i])); - } - siderust_phase_events_free(ptr, count); - return result; +inline std::vector phase_events_from_c(siderust_phase_event_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(PhaseEvent::from_c(ptr[i])); + } + siderust_phase_events_free(ptr, count); + return result; } /// Like periods_from_c but for tempoch_period_mjd_t* pointers (freed with /// siderust_periods_free). -inline std::vector illum_periods_from_c( - tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); - } - siderust_periods_free(ptr, count); - return result; +inline std::vector illum_periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; } } // namespace detail @@ -121,11 +121,11 @@ namespace moon { * * @param jd Julian Date (e.g. `siderust::JulianDate(2451545.0)` for J2000.0). */ -inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { - siderust_moon_phase_geometry_t out{}; - check_status(siderust_moon_phase_geocentric(jd.value(), &out), - "moon::phase_geocentric"); - return MoonPhaseGeometry::from_c(out); +inline MoonPhaseGeometry phase_geocentric(const JulianDate &jd) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_geocentric(jd.value(), &out), + "moon::phase_geocentric"); + return MoonPhaseGeometry::from_c(out); } /** @@ -134,51 +134,53 @@ inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { * @param jd Julian Date. * @param site Observer geodetic coordinates. */ -inline MoonPhaseGeometry phase_topocentric(const JulianDate& jd, - const Geodetic& site) { - siderust_moon_phase_geometry_t out{}; - check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), - "moon::phase_topocentric"); - return MoonPhaseGeometry::from_c(out); +inline MoonPhaseGeometry phase_topocentric(const JulianDate &jd, + const Geodetic &site) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), + "moon::phase_topocentric"); + return MoonPhaseGeometry::from_c(out); } /** * @brief Determine the descriptive phase label for a given geometry. * - * @param geom Moon phase geometry (as returned by phase_geocentric / phase_topocentric). + * @param geom Moon phase geometry (as returned by phase_geocentric / + * phase_topocentric). */ -inline MoonPhaseLabel phase_label(const MoonPhaseGeometry& geom) { - siderust_moon_phase_geometry_t c{geom.phase_angle_rad, - geom.illuminated_fraction, - geom.elongation_rad, - static_cast(geom.waxing)}; - siderust_moon_phase_label_t out{}; - check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); - return static_cast(out); +inline MoonPhaseLabel phase_label(const MoonPhaseGeometry &geom) { + siderust_moon_phase_geometry_t c{ + geom.phase_angle_rad, geom.illuminated_fraction, geom.elongation_rad, + static_cast(geom.waxing)}; + siderust_moon_phase_label_t out{}; + check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); + return static_cast(out); } /** - * @brief Find principal phase events (new moon, quarters, full moon) in a window. + * @brief Find principal phase events (new moon, quarters, full moon) in a + * window. * * @param window MJD search window. * @param opts Search tolerances (optional). */ -inline std::vector find_phase_events( - const Period& window, const SearchOptions& opts = {}) { - siderust_phase_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_find_phase_events( - window.c_inner(), opts.to_c(), &ptr, &count), - "moon::find_phase_events"); - return detail::phase_events_from_c(ptr, count); +inline std::vector +find_phase_events(const Period &window, const SearchOptions &opts = {}) { + siderust_phase_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status( + siderust_find_phase_events(window.c_inner(), opts.to_c(), &ptr, &count), + "moon::find_phase_events"); + return detail::phase_events_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector find_phase_events( - const MJD& start, const MJD& end, const SearchOptions& opts = {}) { - return find_phase_events(Period(start, end), opts); +inline std::vector +find_phase_events(const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return find_phase_events(Period(start, end), opts); } /** @@ -188,23 +190,24 @@ inline std::vector find_phase_events( * @param k_min Minimum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_above( - const Period& window, double k_min, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_above( - window.c_inner(), k_min, opts.to_c(), &ptr, &count), - "moon::illumination_above"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_above(const Period &window, + double k_min, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_above(window.c_inner(), k_min, + opts.to_c(), &ptr, &count), + "moon::illumination_above"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_above( - const MJD& start, const MJD& end, double k_min, - const SearchOptions& opts = {}) { - return illumination_above(Period(start, end), k_min, opts); +inline std::vector illumination_above(const MJD &start, const MJD &end, + double k_min, + const SearchOptions &opts = {}) { + return illumination_above(Period(start, end), k_min, opts); } /** @@ -214,23 +217,24 @@ inline std::vector illumination_above( * @param k_max Maximum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_below( - const Period& window, double k_max, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_below( - window.c_inner(), k_max, opts.to_c(), &ptr, &count), - "moon::illumination_below"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_below(const Period &window, + double k_max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_below(window.c_inner(), k_max, + opts.to_c(), &ptr, &count), + "moon::illumination_below"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_below( - const MJD& start, const MJD& end, double k_max, - const SearchOptions& opts = {}) { - return illumination_below(Period(start, end), k_max, opts); +inline std::vector illumination_below(const MJD &start, const MJD &end, + double k_max, + const SearchOptions &opts = {}) { + return illumination_below(Period(start, end), k_max, opts); } /** @@ -241,24 +245,24 @@ inline std::vector illumination_below( * @param k_max Maximum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_range( - const Period& window, double k_min, double k_max, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_range( - window.c_inner(), k_min, k_max, opts.to_c(), &ptr, &count), - "moon::illumination_range"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_range(const Period &window, + double k_min, double k_max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_range(window.c_inner(), k_min, k_max, + opts.to_c(), &ptr, &count), + "moon::illumination_range"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_range( - const MJD& start, const MJD& end, double k_min, double k_max, - const SearchOptions& opts = {}) { - return illumination_range(Period(start, end), k_min, k_max, opts); +inline std::vector illumination_range(const MJD &start, const MJD &end, + double k_min, double k_max, + const SearchOptions &opts = {}) { + return illumination_range(Period(start, end), k_min, k_max, opts); } } // namespace moon @@ -270,36 +274,36 @@ inline std::vector illumination_range( /** * @brief Get the illuminated fraction as a percentage [0, 100]. */ -inline double illuminated_percent(const MoonPhaseGeometry& geom) { - return geom.illuminated_fraction * 100.0; +inline double illuminated_percent(const MoonPhaseGeometry &geom) { + return geom.illuminated_fraction * 100.0; } /** * @brief Check if a phase label describes a waxing moon. */ inline bool is_waxing(MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::WaxingCrescent: - case MoonPhaseLabel::FirstQuarter: - case MoonPhaseLabel::WaxingGibbous: - return true; - default: - return false; - } + switch (label) { + case MoonPhaseLabel::WaxingCrescent: + case MoonPhaseLabel::FirstQuarter: + case MoonPhaseLabel::WaxingGibbous: + return true; + default: + return false; + } } /** * @brief Check if a phase label describes a waning moon. */ inline bool is_waning(MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::WaningGibbous: - case MoonPhaseLabel::LastQuarter: - case MoonPhaseLabel::WaningCrescent: - return true; - default: - return false; - } + switch (label) { + case MoonPhaseLabel::WaningGibbous: + case MoonPhaseLabel::LastQuarter: + case MoonPhaseLabel::WaningCrescent: + return true; + default: + return false; + } } // ============================================================================ @@ -309,43 +313,43 @@ inline bool is_waning(MoonPhaseLabel label) { /** * @brief Stream operator for PhaseKind. */ -inline std::ostream& operator<<(std::ostream& os, PhaseKind kind) { - switch (kind) { - case PhaseKind::NewMoon: - return os << "new moon"; - case PhaseKind::FirstQuarter: - return os << "first quarter"; - case PhaseKind::FullMoon: - return os << "full moon"; - case PhaseKind::LastQuarter: - return os << "last quarter"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, PhaseKind kind) { + switch (kind) { + case PhaseKind::NewMoon: + return os << "new moon"; + case PhaseKind::FirstQuarter: + return os << "first quarter"; + case PhaseKind::FullMoon: + return os << "full moon"; + case PhaseKind::LastQuarter: + return os << "last quarter"; + } + return os << "unknown"; } /** * @brief Stream operator for MoonPhaseLabel. */ -inline std::ostream& operator<<(std::ostream& os, MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::NewMoon: - return os << "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return os << "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return os << "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return os << "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return os << "full moon"; - case MoonPhaseLabel::WaningGibbous: - return os << "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return os << "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return os << "waning crescent"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::NewMoon: + return os << "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return os << "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return os << "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return os << "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return os << "full moon"; + case MoonPhaseLabel::WaningGibbous: + return os << "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return os << "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return os << "waning crescent"; + } + return os << "unknown"; } } // namespace siderust diff --git a/include/siderust/observatories.hpp b/include/siderust/observatories.hpp index 5e9c345..e632906 100644 --- a/include/siderust/observatories.hpp +++ b/include/siderust/observatories.hpp @@ -13,27 +13,28 @@ namespace siderust { namespace detail { inline Geodetic make_roque_de_los_muchachos() { - siderust_geodetic_t out; - check_status(siderust_observatory_roque_de_los_muchachos(&out), "ROQUE_DE_LOS_MUCHACHOS"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_roque_de_los_muchachos(&out), + "ROQUE_DE_LOS_MUCHACHOS"); + return Geodetic::from_c(out); } inline Geodetic make_el_paranal() { - siderust_geodetic_t out; - check_status(siderust_observatory_el_paranal(&out), "EL_PARANAL"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_el_paranal(&out), "EL_PARANAL"); + return Geodetic::from_c(out); } inline Geodetic make_mauna_kea() { - siderust_geodetic_t out; - check_status(siderust_observatory_mauna_kea(&out), "MAUNA_KEA"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_mauna_kea(&out), "MAUNA_KEA"); + return Geodetic::from_c(out); } inline Geodetic make_la_silla() { - siderust_geodetic_t out; - check_status(siderust_observatory_la_silla(&out), "LA_SILLA_OBSERVATORY"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_la_silla(&out), "LA_SILLA_OBSERVATORY"); + return Geodetic::from_c(out); } } // namespace detail @@ -41,17 +42,19 @@ inline Geodetic make_la_silla() { /** * @brief Create a custom geodetic position (WGS84). */ -inline Geodetic geodetic(double lon_deg, double lat_deg, double height_m = 0.0) { - siderust_geodetic_t out; - check_status(siderust_geodetic_new(lon_deg, lat_deg, height_m, &out), - "geodetic"); - return Geodetic::from_c(out); +inline Geodetic geodetic(double lon_deg, double lat_deg, + double height_m = 0.0) { + siderust_geodetic_t out; + check_status(siderust_geodetic_new(lon_deg, lat_deg, height_m, &out), + "geodetic"); + return Geodetic::from_c(out); } /** * @brief Roque de los Muchachos Observatory (La Palma, Spain). */ -inline const Geodetic ROQUE_DE_LOS_MUCHACHOS = detail::make_roque_de_los_muchachos(); +inline const Geodetic ROQUE_DE_LOS_MUCHACHOS = + detail::make_roque_de_los_muchachos(); /** * @brief El Paranal Observatory (Chile). diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 483bd6c..63e0c05 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,16 +13,19 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); - * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), + * qtty::Degree(38.78369)); auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, + * 0}); * * // Template-targeted transform — invalid pairs won't compile - * auto ecl = vega_icrs.to_frame(jd); // Direction - * auto hor = vega_icrs.to_horizontal(jd, ROQUE_DE_LOS_MUCHACHOS); + * auto ecl = vega_icrs.to_frame(jd); // + * Direction auto hor = vega_icrs.to_horizontal(jd, + * ROQUE_DE_LOS_MUCHACHOS); * * // Typed ephemeris — unit-safe AU/km positions - * auto earth = ephemeris::earth_heliocentric(jd); // cartesian::Position - * auto dist = earth.comp_x.to(); // unit conversion + * auto earth = ephemeris::earth_heliocentric(jd); // + * cartesian::Position auto dist = + * earth.comp_x.to(); // unit conversion * @endcode */ diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp index 72e336a..40f0a02 100644 --- a/include/siderust/star_target.hpp +++ b/include/siderust/star_target.hpp @@ -30,66 +30,69 @@ namespace siderust { * globals and live for the entire program. */ class StarTarget : public Trackable { - public: - /** - * @brief Wrap a Star reference as a Trackable. - * @param star Reference to a Star. Must outlive this adapter. - */ - explicit StarTarget(const Star& star) : star_(star) {} - - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ - - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - // star_altitude::altitude_at returns Radian; convert to Degree - auto rad = star_altitude::altitude_at(star_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::above_threshold(star_, obs, window, threshold, opts); - } - - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::below_threshold(star_, obs, window, threshold, opts); - } - - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::crossings(star_, obs, window, threshold, opts); - } - - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - return star_altitude::culminations(star_, obs, window, opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ - - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - return star_altitude::azimuth_at(star_, obs, mjd); - } - - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); - } - - /// Access the underlying Star reference. - const Star& star() const { return star_; } - - private: - const Star& star_; +public: + /** + * @brief Wrap a Star reference as a Trackable. + * @param star Reference to a Star. Must outlive this adapter. + */ + explicit StarTarget(const Star &star) : star_(star) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + // star_altitude::altitude_at returns Radian; convert to Degree + auto rad = star_altitude::altitude_at(star_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::above_threshold(star_, obs, window, threshold, opts); + } + + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::below_threshold(star_, obs, window, threshold, opts); + } + + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::crossings(star_, obs, window, threshold, opts); + } + + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + return star_altitude::culminations(star_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + return star_altitude::azimuth_at(star_, obs, mjd); + } + + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); + } + + /// Access the underlying Star reference. + const Star &star() const { return star_; } + +private: + const Star &star_; }; } // namespace siderust diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index c7d6bc9..af7fba3 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -49,22 +49,21 @@ namespace detail { /// @cond INTERNAL /// True iff T is an instantiation of spherical::Direction. -template -struct is_spherical_direction : std::false_type {}; +template struct is_spherical_direction : std::false_type {}; template struct is_spherical_direction> : std::true_type {}; template -inline constexpr bool is_spherical_direction_v = is_spherical_direction::value; +inline constexpr bool is_spherical_direction_v = + is_spherical_direction::value; /// Extract the frame tag F from spherical::Direction. -template -struct spherical_direction_frame; // undefined primary +template struct spherical_direction_frame; // undefined primary template struct spherical_direction_frame> { - using type = F; + using type = F; }; template @@ -98,276 +97,284 @@ using spherical_direction_frame_t = typename spherical_direction_frame::type; * auto alt = ec.altitude_at(obs, now); * @endcode */ -template -class Target : public Trackable { - - static_assert(detail::is_spherical_direction_v, - "Target: C must be a specialisation of " - "siderust::spherical::Direction"); - - using Frame = detail::spherical_direction_frame_t; - - static_assert(frames::has_frame_transform_v, - "Target: frame F must support a transform to ICRS " - "(frames::has_frame_transform_v must be true). " - "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " - "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); - - public: - // ------------------------------------------------------------------ - // Construction / destruction - // ------------------------------------------------------------------ - - /** - * @brief Construct from a strongly-typed spherical direction. - * - * For frames other than ICRS, the direction is converted to ICRS before - * being registered with the Rust FFI. The original `C` direction is - * retained for C++-side accessors. - * - * @param dir Spherical direction (any supported frame). - * @param epoch Coordinate epoch (default J2000.0). - */ - explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) - : m_dir_(dir), m_epoch_(epoch) { - // Convert to ICRS for the FFI; identity transform when already ICRS. - if constexpr (std::is_same_v) { - m_icrs_ = dir; - } else { - m_icrs_ = dir.template to_frame(epoch); - } - SiderustTarget* h = nullptr; - check_status( - siderust_target_create( - m_icrs_.ra().value(), m_icrs_.dec().value(), epoch.value(), &h), - "Target::Target"); - handle_ = h; - } - - ~Target() { - if (handle_) { - siderust_target_free(handle_); - handle_ = nullptr; - } - } - - /// Move constructor. - Target(Target&& other) noexcept - : m_dir_(std::move(other.m_dir_)), - m_epoch_(other.m_epoch_), - m_icrs_(other.m_icrs_), - handle_(other.handle_) { - other.handle_ = nullptr; - } - - /// Move assignment. - Target& operator=(Target&& other) noexcept { - if (this != &other) { - if (handle_) { - siderust_target_free(handle_); - } - m_dir_ = std::move(other.m_dir_); - m_epoch_ = other.m_epoch_; - m_icrs_ = other.m_icrs_; - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; +template class Target : public Trackable { + + static_assert(detail::is_spherical_direction_v, + "Target: C must be a specialisation of " + "siderust::spherical::Direction"); + + using Frame = detail::spherical_direction_frame_t; + + static_assert( + frames::has_frame_transform_v, + "Target: frame F must support a transform to ICRS " + "(frames::has_frame_transform_v must be true). " + "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " + "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); + +public: + // ------------------------------------------------------------------ + // Construction / destruction + // ------------------------------------------------------------------ + + /** + * @brief Construct from a strongly-typed spherical direction. + * + * For frames other than ICRS, the direction is converted to ICRS before + * being registered with the Rust FFI. The original `C` direction is + * retained for C++-side accessors. + * + * @param dir Spherical direction (any supported frame). + * @param epoch Coordinate epoch (default J2000.0). + */ + explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) + : m_dir_(dir), m_epoch_(epoch) { + // Convert to ICRS for the FFI; identity transform when already ICRS. + if constexpr (std::is_same_v) { + m_icrs_ = dir; + } else { + m_icrs_ = dir.template to_frame(epoch); } - - // Prevent copying (the handle has unique ownership). - Target(const Target&) = delete; - Target& operator=(const Target&) = delete; - - // ------------------------------------------------------------------ - // Coordinate accessors - // ------------------------------------------------------------------ - - /// The original typed direction as supplied at construction. - const C& direction() const { return m_dir_; } - - /// Epoch of the coordinate. - JulianDate epoch() const { return m_epoch_; } - - /// The ICRS direction used for FFI calls (equals `direction()` when C is - /// already `spherical::direction::ICRS`). - const spherical::direction::ICRS& icrs_direction() const { return m_icrs_; } - - /// Right ascension — only available for equatorial frames (RA/Dec). - template , int> = 0> - qtty::Degree ra() const { return m_dir_.ra(); } - - /// Declination — only available for equatorial frames (RA/Dec). - template , int> = 0> - qtty::Degree dec() const { return m_dir_.dec(); } - - // ------------------------------------------------------------------ - // Altitude queries (implements Trackable) - // ------------------------------------------------------------------ - - /** - * @brief Compute altitude (degrees) at a given MJD instant. - * - * @note The Rust FFI returns radians; this method converts to degrees. - */ - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - double out{}; - check_status(siderust_target_altitude_at( - handle_, obs.to_c(), mjd.value(), &out), - "Target::altitude_at"); - return qtty::Radian(out).to(); - } - - /** - * @brief Find periods when the target is above a threshold altitude. - */ - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_above_threshold( - handle_, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::above_threshold"); - return detail_periods_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return above_threshold(obs, Period(start, end), threshold, opts); - } - - /** - * @brief Find periods when the target is below a threshold altitude. - */ - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - // Always pass ICRS direction to the FFI layer. - siderust_spherical_dir_t dir_c{}; - dir_c.polar_deg = m_icrs_.dec().value(); - dir_c.azimuth_deg = m_icrs_.ra().value(); - dir_c.frame = SIDERUST_FRAME_T_ICRS; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_below_threshold( - dir_c, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::below_threshold"); - return detail_periods_from_c(ptr, count); + SiderustTarget *h = nullptr; + check_status(siderust_target_create(m_icrs_.ra().value(), + m_icrs_.dec().value(), epoch.value(), + &h), + "Target::Target"); + handle_ = h; + } + + ~Target() { + if (handle_) { + siderust_target_free(handle_); + handle_ = nullptr; } - - /// Backward-compatible [start, end] overload. - std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return below_threshold(obs, Period(start, end), threshold, opts); + } + + /// Move constructor. + Target(Target &&other) noexcept + : m_dir_(std::move(other.m_dir_)), m_epoch_(other.m_epoch_), + m_icrs_(other.m_icrs_), handle_(other.handle_) { + other.handle_ = nullptr; + } + + /// Move assignment. + Target &operator=(Target &&other) noexcept { + if (this != &other) { + if (handle_) { + siderust_target_free(handle_); + } + m_dir_ = std::move(other.m_dir_); + m_epoch_ = other.m_epoch_; + m_icrs_ = other.m_icrs_; + handle_ = other.handle_; + other.handle_ = nullptr; } - - /** - * @brief Find threshold-crossing events (rising / setting). - */ - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_crossings( - handle_, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::crossings"); - return detail::crossings_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return crossings(obs, Period(start, end), threshold, opts); - } - - /** - * @brief Find culmination (local altitude extremum) events. - */ - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_culminations( - handle_, obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "Target::culminations"); - return detail::culminations_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) const { - return culminations(obs, Period(start, end), opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries (implements Trackable) - // ------------------------------------------------------------------ - - /** - * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. - */ - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - double out{}; - check_status(siderust_target_azimuth_at( - handle_, obs.to_c(), mjd.value(), &out), - "Target::azimuth_at"); - return qtty::Degree(out); - } - - /** - * @brief Find epochs when the target crosses a given azimuth bearing. - */ - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_azimuth_crossings( - handle_, obs.to_c(), window.c_inner(), - bearing.value(), opts.to_c(), &ptr, &count), - "Target::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) const { - return azimuth_crossings(obs, Period(start, end), bearing, opts); - } - - /// Access the underlying C handle (advanced use). - const SiderustTarget* c_handle() const { return handle_; } - - private: - C m_dir_; - JulianDate m_epoch_; - spherical::direction::ICRS m_icrs_; - SiderustTarget* handle_ = nullptr; - - /// Build a Period vector from a tempoch_period_mjd_t* array. - static std::vector detail_periods_from_c( - tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); - } - siderust_periods_free(ptr, count); - return result; + return *this; + } + + // Prevent copying (the handle has unique ownership). + Target(const Target &) = delete; + Target &operator=(const Target &) = delete; + + // ------------------------------------------------------------------ + // Coordinate accessors + // ------------------------------------------------------------------ + + /// The original typed direction as supplied at construction. + const C &direction() const { return m_dir_; } + + /// Epoch of the coordinate. + JulianDate epoch() const { return m_epoch_; } + + /// The ICRS direction used for FFI calls (equals `direction()` when C is + /// already `spherical::direction::ICRS`). + const spherical::direction::ICRS &icrs_direction() const { return m_icrs_; } + + /// Right ascension — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree ra() const { + return m_dir_.ra(); + } + + /// Declination — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree dec() const { + return m_dir_.dec(); + } + + // ------------------------------------------------------------------ + // Altitude queries (implements Trackable) + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude (degrees) at a given MJD instant. + * + * @note The Rust FFI returns radians; this method converts to degrees. + */ + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + double out{}; + check_status( + siderust_target_altitude_at(handle_, obs.to_c(), mjd.value(), &out), + "Target::altitude_at"); + return qtty::Radian(out).to(); + } + + /** + * @brief Find periods when the target is above a threshold altitude. + */ + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_above_threshold( + handle_, obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::above_threshold"); + return detail_periods_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector above_threshold(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return above_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find periods when the target is below a threshold altitude. + */ + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + // Always pass ICRS direction to the FFI layer. + siderust_spherical_dir_t dir_c{}; + dir_c.polar_deg = m_icrs_.dec().value(); + dir_c.azimuth_deg = m_icrs_.ra().value(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir_c, obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::below_threshold"); + return detail_periods_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector below_threshold(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return below_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_crossings(handle_, obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::crossings"); + return detail::crossings_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector crossings(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return crossings(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find culmination (local altitude extremum) events. + */ + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_culminations(handle_, obs.to_c(), + window.c_inner(), opts.to_c(), + &ptr, &count), + "Target::culminations"); + return detail::culminations_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector + culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) const { + return culminations(obs, Period(start, end), opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries (implements Trackable) + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + double out{}; + check_status( + siderust_target_azimuth_at(handle_, obs.to_c(), mjd.value(), &out), + "Target::azimuth_at"); + return qtty::Degree(out); + } + + /** + * @brief Find epochs when the target crosses a given azimuth bearing. + */ + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_azimuth_crossings( + handle_, obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "Target::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector + azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, + const SearchOptions &opts = {}) const { + return azimuth_crossings(obs, Period(start, end), bearing, opts); + } + + /// Access the underlying C handle (advanced use). + const SiderustTarget *c_handle() const { return handle_; } + +private: + C m_dir_; + JulianDate m_epoch_; + spherical::direction::ICRS m_icrs_; + SiderustTarget *handle_ = nullptr; + + /// Build a Period vector from a tempoch_period_mjd_t* array. + static std::vector detail_periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); } + siderust_periods_free(ptr, count); + return result; + } }; // ============================================================================ @@ -381,13 +388,17 @@ using ICRSTarget = Target; using ICRFTarget = Target; /// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). -using EquatorialMeanJ2000Target = Target; +using EquatorialMeanJ2000Target = + Target; /// Fixed direction in mean equatorial coordinates of date (precessed only). -using EquatorialMeanOfDateTarget = Target; +using EquatorialMeanOfDateTarget = + Target; -/// Fixed direction in true equatorial coordinates of date (precessed + nutated). -using EquatorialTrueOfDateTarget = Target; +/// Fixed direction in true equatorial coordinates of date (precessed + +/// nutated). +using EquatorialTrueOfDateTarget = + Target; /// Fixed direction in mean ecliptic coordinates of J2000.0. using EclipticMeanJ2000Target = Target; diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index ddef029..126f781 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -13,10 +13,10 @@ namespace siderust { -using CivilTime = tempoch::CivilTime; -using UTC = tempoch::UTC; // alias for CivilTime -using JulianDate = tempoch::JulianDate; // Time -using MJD = tempoch::MJD; // Time -using Period = tempoch::Period; +using CivilTime = tempoch::CivilTime; +using UTC = tempoch::UTC; // alias for CivilTime +using JulianDate = tempoch::JulianDate; // Time +using MJD = tempoch::MJD; // Time +using Period = tempoch::Period; } // namespace siderust diff --git a/include/siderust/trackable.hpp b/include/siderust/trackable.hpp index 5aefe37..08a236f 100644 --- a/include/siderust/trackable.hpp +++ b/include/siderust/trackable.hpp @@ -40,80 +40,86 @@ namespace siderust { /** - * @brief Abstract interface for any object whose altitude/azimuth can be computed. + * @brief Abstract interface for any object whose altitude/azimuth can be + * computed. * * This class defines the common API shared by all trackable celestial objects. * Implementations must provide altitude_at and azimuth_at at minimum; the * remaining methods have default implementations that throw if not overridden. */ class Trackable { - public: - virtual ~Trackable() = default; +public: + virtual ~Trackable() = default; - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ - /** - * @brief Compute altitude at a given MJD instant. - * - * The return unit varies by implementation (radians for sun/moon/star, - * degrees for Target/BodyTarget). Check the concrete class documentation. - * - * @note For BodyTarget, returns radians; for Target, returns degrees. - */ - virtual qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const = 0; + /** + * @brief Compute altitude at a given MJD instant. + * + * The return unit varies by implementation (radians for sun/moon/star, + * degrees for Target/BodyTarget). Check the concrete class documentation. + * + * @note For BodyTarget, returns radians; for Target, returns degrees. + */ + virtual qtty::Degree altitude_at(const Geodetic &obs, + const MJD &mjd) const = 0; - /** - * @brief Find periods when the object is above a threshold altitude. - */ - virtual std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find periods when the object is above a threshold altitude. + */ + virtual std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find periods when the object is below a threshold altitude. - */ - virtual std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find periods when the object is below a threshold altitude. + */ + virtual std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find threshold-crossing events (rising / setting). - */ - virtual std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find threshold-crossing events (rising / setting). + */ + virtual std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find culmination (local altitude extremum) events. - */ - virtual std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const = 0; + /** + * @brief Find culmination (local altitude extremum) events. + */ + virtual std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const = 0; - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ - /** - * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. - */ - virtual qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const = 0; + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + virtual qtty::Degree azimuth_at(const Geodetic &obs, + const MJD &mjd) const = 0; - /** - * @brief Find epochs when the object crosses a given azimuth bearing. - */ - virtual std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find epochs when the object crosses a given azimuth bearing. + */ + virtual std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const = 0; - // Non-copyable, non-movable from base - Trackable() = default; - Trackable(const Trackable&) = delete; - Trackable& operator=(const Trackable&) = delete; - Trackable(Trackable&&) = default; - Trackable& operator=(Trackable&&) = default; + // Non-copyable, non-movable from base + Trackable() = default; + Trackable(const Trackable &) = delete; + Trackable &operator=(const Trackable &) = delete; + Trackable(Trackable &&) = default; + Trackable &operator=(Trackable &&) = default; }; } // namespace siderust diff --git a/tests/main.cpp b/tests/main.cpp index 5ebbc76..4d820af 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,6 +1,6 @@ #include -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 72b2419..6121c62 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -7,18 +7,18 @@ using namespace siderust; static const double PI = 3.14159265358979323846; class AltitudeTest : public ::testing::Test { - protected: - Geodetic obs; - MJD start; - MJD end_; - Period window{MJD(0.0), MJD(1.0)}; - - void SetUp() override { - obs = ROQUE_DE_LOS_MUCHACHOS; - start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); - end_ = start + qtty::Day(1.0); // 24 hours - window = Period(start, end_); - } +protected: + Geodetic obs; + MJD start; + MJD end_; + Period window{MJD(0.0), MJD(1.0)}; + + void SetUp() override { + obs = ROQUE_DE_LOS_MUCHACHOS; + start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); + end_ = start + qtty::Day(1.0); // 24 hours + window = Period(start, end_); + } }; // ============================================================================ @@ -26,49 +26,50 @@ class AltitudeTest : public ::testing::Test { // ============================================================================ TEST_F(AltitudeTest, SunAltitudeAt) { - qtty::Radian alt = sun::altitude_at(obs, start); - // Should be a valid radian value - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + qtty::Radian alt = sun::altitude_at(obs, start); + // Should be a valid radian value + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, SunAboveThreshold) { - // Find periods when sun > 0 deg (daytime) - auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); - EXPECT_GT(periods.size(), 0u); - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Find periods when sun > 0 deg (daytime) + auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); + EXPECT_GT(periods.size(), 0u); + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } TEST_F(AltitudeTest, SunBelowThreshold) { - // Astronomical night: sun < -18° - auto periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); - // In July at La Palma, astronomical night may be short but should exist - // (or possibly not if too close to solstice — accept 0+) - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Astronomical night: sun < -18° + auto periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); + // In July at La Palma, astronomical night may be short but should exist + // (or possibly not if too close to solstice — accept 0+) + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } TEST_F(AltitudeTest, SunCrossings) { - auto events = sun::crossings(obs, window, qtty::Degree(0.0)); - // Expect at least 1 crossing in 24h (sunrise or sunset) - EXPECT_GE(events.size(), 1u); + auto events = sun::crossings(obs, window, qtty::Degree(0.0)); + // Expect at least 1 crossing in 24h (sunrise or sunset) + EXPECT_GE(events.size(), 1u); } TEST_F(AltitudeTest, SunCulminations) { - auto events = sun::culminations(obs, window); - // At least one culmination (meridian passage) - EXPECT_GE(events.size(), 1u); + auto events = sun::culminations(obs, window); + // At least one culmination (meridian passage) + EXPECT_GE(events.size(), 1u); } TEST_F(AltitudeTest, SunAltitudePeriods) { - // Find periods when sun is between -6° and 0° (civil twilight) - auto periods = sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Find periods when sun is between -6° and 0° (civil twilight) + auto periods = + sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } // ============================================================================ @@ -76,17 +77,17 @@ TEST_F(AltitudeTest, SunAltitudePeriods) { // ============================================================================ TEST_F(AltitudeTest, MoonAltitudeAt) { - qtty::Radian alt = moon::altitude_at(obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + qtty::Radian alt = moon::altitude_at(obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, MoonAboveThreshold) { - auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); - // Moon may or may not be above horizon for this date; just no crash - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); + // Moon may or may not be above horizon for this date; just no crash + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } // ============================================================================ @@ -94,17 +95,18 @@ TEST_F(AltitudeTest, MoonAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, StarAltitudeAt) { - const auto& vega = VEGA; - qtty::Radian alt = star_altitude::altitude_at(vega, obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + const auto &vega = VEGA; + qtty::Radian alt = star_altitude::altitude_at(vega, obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, StarAboveThreshold) { - const auto& vega = VEGA; - auto periods = star_altitude::above_threshold(vega, obs, window, qtty::Degree(30.0)); - // Vega should be well above 30° from La Palma in July - EXPECT_GT(periods.size(), 0u); + const auto &vega = VEGA; + auto periods = + star_altitude::above_threshold(vega, obs, window, qtty::Degree(30.0)); + // Vega should be well above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); } // ============================================================================ @@ -112,17 +114,19 @@ TEST_F(AltitudeTest, StarAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, IcrsAltitudeAt) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - qtty::Radian alt = icrs_altitude::altitude_at(vega_icrs, obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), + qtty::Degree(38.78)); + qtty::Radian alt = icrs_altitude::altitude_at(vega_icrs, obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, IcrsAboveThreshold) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - auto periods = icrs_altitude::above_threshold( - vega_icrs, obs, window, qtty::Degree(30.0)); - EXPECT_GT(periods.size(), 0u); + const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), + qtty::Degree(38.78)); + auto periods = icrs_altitude::above_threshold(vega_icrs, obs, window, + qtty::Degree(30.0)); + EXPECT_GT(periods.size(), 0u); } // ============================================================================ @@ -131,62 +135,62 @@ TEST_F(AltitudeTest, IcrsAboveThreshold) { // Vega ICRS coordinates (J2000): RA=279.2348°, Dec=+38.7836° TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - // altitude_at returns qtty::Degree (radian/degree bug-fix verification) - qtty::Degree alt = vega.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + // altitude_at returns qtty::Degree (radian/degree bug-fix verification) + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); - // Vega should rise above 30° from La Palma in July - EXPECT_GT(periods.size(), 0u); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); + // Vega should rise above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); } TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); - EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); - // epoch defaults to J2000 - EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); - // icrs_direction is the same for an ICRS Target - EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); + EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); + // epoch defaults to J2000 + EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); + // icrs_direction is the same for an ICRS Target + EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); } TEST_F(AltitudeTest, ICRSTargetPolymorphic) { - // Verify Target is usable through the Trackable interface - std::unique_ptr t = std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23), qtty::Degree(38.78) }); - qtty::Degree alt = t->altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + // Verify Target is usable through the Trackable interface + std::unique_ptr t = std::make_unique( + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}); + qtty::Degree alt = t->altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { - // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° - EclipticMeanJ2000Target ec{ spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8) } }; - // ecl direction retained on the C++ side - EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); - EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); - // ICRS ra/dec computed at construction and accessible - EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); - EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); - // altitude should be a valid degree value - qtty::Degree alt = ec.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° + EclipticMeanJ2000Target ec{spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8)}}; + // ecl direction retained on the C++ side + EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); + EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); + // ICRS ra/dec computed at construction and accessible + EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); + EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); + // altitude should be a valid degree value + qtty::Degree alt = ec.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, EquatorialMeanJ2000TargetAltitudeAt) { - EquatorialMeanJ2000Target vega{ spherical::direction::EquatorialMeanJ2000{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - qtty::Degree alt = vega.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + EquatorialMeanJ2000Target vega{spherical::direction::EquatorialMeanJ2000{ + qtty::Degree(279.23), qtty::Degree(38.78)}}; + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 530617e..c4cf2a5 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -8,51 +8,50 @@ using namespace siderust; // ============================================================================ TEST(Bodies, StarCatalogVega) { - const auto& vega = VEGA; - EXPECT_EQ(vega.name(), "Vega"); - EXPECT_NEAR(vega.distance_ly(), 25.0, 1.0); - EXPECT_GT(vega.luminosity_solar(), 1.0); + const auto &vega = VEGA; + EXPECT_EQ(vega.name(), "Vega"); + EXPECT_NEAR(vega.distance_ly(), 25.0, 1.0); + EXPECT_GT(vega.luminosity_solar(), 1.0); } TEST(Bodies, StarCatalogSirius) { - const auto& sirius = SIRIUS; - EXPECT_EQ(sirius.name(), "Sirius"); - EXPECT_NEAR(sirius.distance_ly(), 8.6, 0.5); + const auto &sirius = SIRIUS; + EXPECT_EQ(sirius.name(), "Sirius"); + EXPECT_NEAR(sirius.distance_ly(), 8.6, 0.5); } TEST(Bodies, StarCatalogUnknownThrows) { - EXPECT_THROW(Star::catalog("NONEXISTENT"), UnknownStarError); + EXPECT_THROW(Star::catalog("NONEXISTENT"), UnknownStarError); } TEST(Bodies, StarMoveSemantics) { - auto s1 = Star::catalog("POLARIS"); - EXPECT_TRUE(static_cast(s1)); + auto s1 = Star::catalog("POLARIS"); + EXPECT_TRUE(static_cast(s1)); - auto s2 = std::move(s1); - EXPECT_TRUE(static_cast(s2)); - // s1 is now empty (moved-from) + auto s2 = std::move(s1); + EXPECT_TRUE(static_cast(s2)); + // s1 is now empty (moved-from) } TEST(Bodies, StarCreate) { - auto s = Star::create( - "TestStar", - 100.0, // distance_ly - 1.0, // mass_solar - 1.0, // radius_solar - 1.0, // luminosity_solar - 180.0, // ra_deg - 45.0, // dec_deg - 2451545.0 // epoch_jd (J2000) - ); - EXPECT_EQ(s.name(), "TestStar"); - EXPECT_NEAR(s.distance_ly(), 100.0, 1e-6); + auto s = Star::create("TestStar", + 100.0, // distance_ly + 1.0, // mass_solar + 1.0, // radius_solar + 1.0, // luminosity_solar + 180.0, // ra_deg + 45.0, // dec_deg + 2451545.0 // epoch_jd (J2000) + ); + EXPECT_EQ(s.name(), "TestStar"); + EXPECT_NEAR(s.distance_ly(), 100.0, 1e-6); } TEST(Bodies, StarCreateWithProperMotion) { - ProperMotion pm(0.001, -0.002, RaConvention::MuAlphaStar); - auto s = Star::create("PMStar", 50.0, 1.0, 1.0, 1.0, - 100.0, 30.0, 2451545.0, pm); - EXPECT_EQ(s.name(), "PMStar"); + ProperMotion pm(0.001, -0.002, RaConvention::MuAlphaStar); + auto s = + Star::create("PMStar", 50.0, 1.0, 1.0, 1.0, 100.0, 30.0, 2451545.0, pm); + EXPECT_EQ(s.name(), "PMStar"); } // ============================================================================ @@ -60,28 +59,28 @@ TEST(Bodies, StarCreateWithProperMotion) { // ============================================================================ TEST(Bodies, PlanetEarth) { - auto e = EARTH; - EXPECT_NEAR(e.mass_kg, 5.972e24, 0.01e24); - EXPECT_NEAR(e.radius_km, 6371.0, 10.0); - EXPECT_NEAR(e.orbit.semi_major_axis_au, 1.0, 0.01); + auto e = EARTH; + EXPECT_NEAR(e.mass_kg, 5.972e24, 0.01e24); + EXPECT_NEAR(e.radius_km, 6371.0, 10.0); + EXPECT_NEAR(e.orbit.semi_major_axis_au, 1.0, 0.01); } TEST(Bodies, PlanetMars) { - auto m = MARS; - EXPECT_GT(m.mass_kg, 0); - EXPECT_NEAR(m.orbit.semi_major_axis_au, 1.524, 0.01); + auto m = MARS; + EXPECT_GT(m.mass_kg, 0); + EXPECT_NEAR(m.orbit.semi_major_axis_au, 1.524, 0.01); } TEST(Bodies, AllPlanets) { - // Ensure all static constants are populated. - EXPECT_GT(MERCURY.mass_kg, 0.0); - EXPECT_GT(VENUS.mass_kg, 0.0); - EXPECT_GT(EARTH.mass_kg, 0.0); - EXPECT_GT(MARS.mass_kg, 0.0); - EXPECT_GT(JUPITER.mass_kg, 0.0); - EXPECT_GT(SATURN.mass_kg, 0.0); - EXPECT_GT(URANUS.mass_kg, 0.0); - EXPECT_GT(NEPTUNE.mass_kg, 0.0); + // Ensure all static constants are populated. + EXPECT_GT(MERCURY.mass_kg, 0.0); + EXPECT_GT(VENUS.mass_kg, 0.0); + EXPECT_GT(EARTH.mass_kg, 0.0); + EXPECT_GT(MARS.mass_kg, 0.0); + EXPECT_GT(JUPITER.mass_kg, 0.0); + EXPECT_GT(SATURN.mass_kg, 0.0); + EXPECT_GT(URANUS.mass_kg, 0.0); + EXPECT_GT(NEPTUNE.mass_kg, 0.0); } // ============================================================================ @@ -89,94 +88,93 @@ TEST(Bodies, AllPlanets) { // ============================================================================ TEST(Bodies, BodyTargetSunAltitude) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = sun.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = sun.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, BodyTargetMarsAltitude) { - BodyTarget mars(Body::Mars); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = mars.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + BodyTarget mars(Body::Mars); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = mars.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, BodyTargetAllBodiesAltitude) { - auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM - auto mjd = MJD(60000.5); - std::vector all = { - Body::Sun, Body::Moon, Body::Mercury, Body::Venus, - Body::Mars, Body::Jupiter, Body::Saturn, Body::Uranus, Body::Neptune - }; - for (auto b : all) { - BodyTarget bt(b); - auto alt = bt.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM + auto mjd = MJD(60000.5); + std::vector all = {Body::Sun, Body::Moon, Body::Mercury, + Body::Venus, Body::Mars, Body::Jupiter, + Body::Saturn, Body::Uranus, Body::Neptune}; + for (auto b : all) { + BodyTarget bt(b); + auto alt = bt.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } TEST(Bodies, BodyTargetAzimuth) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto az = sun.azimuth_at(obs, mjd); - EXPECT_GE(az.value(), 0.0); - EXPECT_LT(az.value(), 360.0); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = sun.azimuth_at(obs, mjd); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); } TEST(Bodies, BodyTargetJupiterAzimuth) { - BodyTarget jup(Body::Jupiter); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto az = jup.azimuth_at(obs, mjd); - EXPECT_TRUE(std::isfinite(az.value())); - EXPECT_GE(az.value(), 0.0); - EXPECT_LT(az.value(), 360.0); + BodyTarget jup(Body::Jupiter); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = jup.azimuth_at(obs, mjd); + EXPECT_TRUE(std::isfinite(az.value())); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); } TEST(Bodies, BodyTargetAboveThreshold) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto window = Period(MJD(60000.0), MJD(60001.0)); - auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); - // Sun should be above horizon for some portion of the day - EXPECT_GT(periods.size(), 0u); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto window = Period(MJD(60000.0), MJD(60001.0)); + auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); + // Sun should be above horizon for some portion of the day + EXPECT_GT(periods.size(), 0u); } TEST(Bodies, BodyTargetPolymorphic) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); - std::vector> targets; - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(Body::Mars)); + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Mars)); - for (const auto& t : targets) { - auto alt = t->altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + for (const auto &t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } TEST(Bodies, BodyNamespaceAltitudeAt) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto rad = body::altitude_at(Body::Saturn, obs, mjd); - EXPECT_TRUE(std::isfinite(rad.value())); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::altitude_at(Body::Saturn, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); } TEST(Bodies, BodyNamespaceAzimuthAt) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto rad = body::azimuth_at(Body::Venus, obs, mjd); - EXPECT_TRUE(std::isfinite(rad.value())); - EXPECT_GE(rad.value(), 0.0); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::azimuth_at(Body::Venus, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); + EXPECT_GE(rad.value(), 0.0); } // ============================================================================ @@ -184,26 +182,26 @@ TEST(Bodies, BodyNamespaceAzimuthAt) { // ============================================================================ TEST(Bodies, StarTargetAltitude) { - const auto& vega = VEGA; - StarTarget st(vega); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = st.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + const auto &vega = VEGA; + StarTarget st(vega); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = st.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); - std::vector> targets; - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(VEGA)); + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(VEGA)); - for (const auto& t : targets) { - auto alt = t->altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + for (const auto &t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 1e6e9d5..5566303 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -10,124 +10,137 @@ using namespace siderust; // ============================================================================ TEST(TypedCoordinates, AliasNamespaces) { - static_assert(std::is_same_v>); - static_assert(std::is_same_v>); - static_assert(std::is_same_v< - spherical::position::ICRS, - spherical::Position>); - static_assert(std::is_same_v< - cartesian::position::ECEF, - cartesian::Position>); + static_assert(std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert(std::is_same_v, + spherical::Position>); + static_assert( + std::is_same_v< + cartesian::position::ECEF, + cartesian::Position>); } TEST(TypedCoordinates, IcrsDirToEcliptic) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS vega(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto jd = JulianDate::J2000(); - // Compile-time typed transform: ICRS -> EclipticMeanJ2000 - auto ecl = vega.to_frame(jd); + // Compile-time typed transform: ICRS -> EclipticMeanJ2000 + auto ecl = vega.to_frame(jd); - // Result is statically typed as Direction - static_assert(std::is_same_v>, - "to_frame must return Direction"); + // Result is statically typed as Direction + static_assert( + std::is_same_v>, + "to_frame must return Direction"); - EXPECT_NEAR(ecl.lat().value(), 61.7, 0.5); + EXPECT_NEAR(ecl.lat().value(), 61.7, 0.5); } TEST(TypedCoordinates, IcrsDirRoundtrip) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto ecl = icrs.to_frame(jd); - auto back = ecl.to_frame(jd); + auto ecl = icrs.to_frame(jd); + auto back = ecl.to_frame(jd); - static_assert(std::is_same_v); - EXPECT_NEAR(back.ra().value(), 100.0, 1e-4); - EXPECT_NEAR(back.dec().value(), 30.0, 1e-4); + static_assert(std::is_same_v); + EXPECT_NEAR(back.ra().value(), 100.0, 1e-4); + EXPECT_NEAR(back.dec().value(), 30.0, 1e-4); } TEST(TypedCoordinates, ToShorthand) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - // .to(jd) is a shorthand for .to_frame(jd) - auto ecl = icrs.to(jd); - static_assert(std::is_same_v>); - EXPECT_NEAR(ecl.lat().value(), 30.0, 30.0); // sanity check — something was computed + // .to(jd) is a shorthand for .to_frame(jd) + auto ecl = icrs.to(jd); + static_assert( + std::is_same_v>); + EXPECT_NEAR(ecl.lat().value(), 30.0, + 30.0); // sanity check — something was computed } TEST(TypedCoordinates, IcrsDirToHorizontal) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto obs = ROQUE_DE_LOS_MUCHACHOS; + spherical::direction::ICRS vega(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto obs = ROQUE_DE_LOS_MUCHACHOS; - auto hor = vega.to_horizontal(jd, obs); + auto hor = vega.to_horizontal(jd, obs); - static_assert(std::is_same_v>); - EXPECT_GT(hor.altitude().value(), -90.0); - EXPECT_LT(hor.altitude().value(), 90.0); + static_assert( + std::is_same_v>); + EXPECT_GT(hor.altitude().value(), -90.0); + EXPECT_LT(hor.altitude().value(), 90.0); } TEST(TypedCoordinates, EquatorialToIcrs) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), + qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto icrs = eq.to_frame(jd); - static_assert(std::is_same_v); + auto icrs = eq.to_frame(jd); + static_assert(std::is_same_v); - // Should be close to input (EquatorialMeanJ2000 ≈ ICRS at J2000) - EXPECT_NEAR(icrs.ra().value(), 100.0, 0.1); - EXPECT_NEAR(icrs.dec().value(), 30.0, 0.1); + // Should be close to input (EquatorialMeanJ2000 ≈ ICRS at J2000) + EXPECT_NEAR(icrs.ra().value(), 100.0, 0.1); + EXPECT_NEAR(icrs.dec().value(), 30.0, 0.1); } TEST(TypedCoordinates, MultiHopTransform) { - using namespace siderust::frames; + using namespace siderust::frames; - // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) - spherical::Direction mean_od(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) + spherical::Direction mean_od(qtty::Degree(100.0), + qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto true_od = mean_od.to_frame(jd); - static_assert(std::is_same_v>); + auto true_od = mean_od.to_frame(jd); + static_assert(std::is_same_v>); - // At J2000, nutation is small — should be close - EXPECT_NEAR(true_od.ra().value(), 100.0, 0.1); - EXPECT_NEAR(true_od.dec().value(), 30.0, 0.1); + // At J2000, nutation is small — should be close + EXPECT_NEAR(true_od.ra().value(), 100.0, 0.1); + EXPECT_NEAR(true_od.dec().value(), 30.0, 0.1); } TEST(TypedCoordinates, SameFrameIdentity) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); + auto jd = JulianDate::J2000(); - auto same = icrs.to_frame(jd); - EXPECT_DOUBLE_EQ(same.ra().value(), 123.456); - EXPECT_DOUBLE_EQ(same.dec().value(), -45.678); + auto same = icrs.to_frame(jd); + EXPECT_DOUBLE_EQ(same.ra().value(), 123.456); + EXPECT_DOUBLE_EQ(same.dec().value(), -45.678); } TEST(TypedCoordinates, QttyDegreeAccessors) { - spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); + spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); - // Frame-specific getters for ICRS. - qtty::Degree ra = d.ra(); - qtty::Degree dec = d.dec(); - EXPECT_DOUBLE_EQ(ra.value(), 123.456); - EXPECT_DOUBLE_EQ(dec.value(), -45.678); + // Frame-specific getters for ICRS. + qtty::Degree ra = d.ra(); + qtty::Degree dec = d.dec(); + EXPECT_DOUBLE_EQ(ra.value(), 123.456); + EXPECT_DOUBLE_EQ(dec.value(), -45.678); - // Convert to radians through qtty - qtty::Radian ra_rad = ra.to(); - EXPECT_NEAR(ra_rad.value(), 123.456 * M_PI / 180.0, 1e-10); + // Convert to radians through qtty + qtty::Radian ra_rad = ra.to(); + EXPECT_NEAR(ra_rad.value(), 123.456 * M_PI / 180.0, 1e-10); } // ============================================================================ @@ -135,21 +148,21 @@ TEST(TypedCoordinates, QttyDegreeAccessors) { // ============================================================================ TEST(TypedCoordinates, GeodeticQttyFields) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; + auto obs = ROQUE_DE_LOS_MUCHACHOS; - // Exercise the qtty::Degree / qtty::Meter fields - qtty::Degree lon = obs.lon; - qtty::Degree lat = obs.lat; - qtty::Meter h = obs.height; + // Exercise the qtty::Degree / qtty::Meter fields + qtty::Degree lon = obs.lon; + qtty::Degree lat = obs.lat; + qtty::Meter h = obs.height; - EXPECT_NE(lon.value(), 0.0); - EXPECT_NE(lat.value(), 0.0); - EXPECT_GT(h.value(), 0.0); + EXPECT_NE(lon.value(), 0.0); + EXPECT_NE(lat.value(), 0.0); + EXPECT_GT(h.value(), 0.0); - // Accessors are the fields themselves - EXPECT_EQ(obs.lon, lon); - EXPECT_EQ(obs.lat, lat); - EXPECT_EQ(obs.height, h); + // Accessors are the fields themselves + EXPECT_EQ(obs.lon, lon); + EXPECT_EQ(obs.lat, lat); + EXPECT_EQ(obs.height, h); } // ============================================================================ @@ -157,28 +170,31 @@ TEST(TypedCoordinates, GeodeticQttyFields) { // ============================================================================ TEST(TypedCoordinates, GeodeticToCartesianEcef) { - auto geo = geodetic(0.0, 0.0, 0.0); - auto cart = geodetic_to_cartesian_ecef(geo); + auto geo = geodetic(0.0, 0.0, 0.0); + auto cart = geodetic_to_cartesian_ecef(geo); - // Typed return: cartesian::Position - static_assert(std::is_same_v>); + // Typed return: cartesian::Position + static_assert( + std::is_same_v>); - EXPECT_NEAR(cart.x().value(), 6378137.0, 1.0); - EXPECT_NEAR(cart.y().value(), 0.0, 1.0); - EXPECT_NEAR(cart.z().value(), 0.0, 1.0); + EXPECT_NEAR(cart.x().value(), 6378137.0, 1.0); + EXPECT_NEAR(cart.y().value(), 0.0, 1.0); + EXPECT_NEAR(cart.z().value(), 0.0, 1.0); } TEST(TypedCoordinates, GeodeticToCartesianMember) { - auto geo = geodetic(0.0, 0.0, 0.0); + auto geo = geodetic(0.0, 0.0, 0.0); - auto ecef_m = geo.to_cartesian(); - auto ecef_km = geo.to_cartesian(); + auto ecef_m = geo.to_cartesian(); + auto ecef_km = geo.to_cartesian(); - static_assert(std::is_same_v>); - static_assert(std::is_same_v< - decltype(ecef_km), - cartesian::Position>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); - EXPECT_NEAR(ecef_m.x().value(), 6378137.0, 1.0); - EXPECT_NEAR(ecef_km.x().value(), 6378.137, 1e-3); + EXPECT_NEAR(ecef_m.x().value(), 6378137.0, 1.0); + EXPECT_NEAR(ecef_km.x().value(), 6378.137, 1e-3); } diff --git a/tests/test_ephemeris.cpp b/tests/test_ephemeris.cpp index fd1489d..73c38d7 100644 --- a/tests/test_ephemeris.cpp +++ b/tests/test_ephemeris.cpp @@ -9,38 +9,42 @@ using namespace siderust; // ============================================================================ TEST(Ephemeris, EarthHeliocentric) { - auto jd = JulianDate::J2000(); - auto pos = ephemeris::earth_heliocentric(jd); - - // Compile-time type checks - static_assert(std::is_same_v< - decltype(pos), - cartesian::position::EclipticMeanJ2000>); - static_assert(std::is_same_v); - - // Value check — distance should be ~1 AU - double r = std::sqrt(pos.x().value() * pos.x().value() + pos.y().value() * pos.y().value() + pos.z().value() * pos.z().value()); - EXPECT_NEAR(r, 1.0, 0.02); - - // Unit conversion: AU -> Kilometer (on individual component) - qtty::Kilometer x_km = pos.comp_x.to(); - // x_km is one component, not the full distance; just verify conversion works - EXPECT_NEAR(x_km.value(), pos.x().value() * 1.495978707e8, 1e3); - - // Total distance in km should be ~1 AU ≈ 149.6M km - double r_km = r * 1.495978707e8; - EXPECT_NEAR(r_km, 1.496e8, 3e6); + auto jd = JulianDate::J2000(); + auto pos = ephemeris::earth_heliocentric(jd); + + // Compile-time type checks + static_assert( + std::is_same_v>); + static_assert(std::is_same_v); + + // Value check — distance should be ~1 AU + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + EXPECT_NEAR(r, 1.0, 0.02); + + // Unit conversion: AU -> Kilometer (on individual component) + qtty::Kilometer x_km = pos.comp_x.to(); + // x_km is one component, not the full distance; just verify conversion works + EXPECT_NEAR(x_km.value(), pos.x().value() * 1.495978707e8, 1e3); + + // Total distance in km should be ~1 AU ≈ 149.6M km + double r_km = r * 1.495978707e8; + EXPECT_NEAR(r_km, 1.496e8, 3e6); } TEST(Ephemeris, MoonGeocentric) { - auto jd = JulianDate::J2000(); - auto pos = ephemeris::moon_geocentric(jd); - - static_assert(std::is_same_v< - decltype(pos), - cartesian::position::MoonGeocentric>); - static_assert(std::is_same_v); - - double r = std::sqrt(pos.x().value() * pos.x().value() + pos.y().value() * pos.y().value() + pos.z().value() * pos.z().value()); - EXPECT_NEAR(r, 384400.0, 25000.0); + auto jd = JulianDate::J2000(); + auto pos = ephemeris::moon_geocentric(jd); + + static_assert( + std::is_same_v>); + static_assert(std::is_same_v); + + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + EXPECT_NEAR(r, 384400.0, 25000.0); } diff --git a/tests/test_observatories.cpp b/tests/test_observatories.cpp index bd673b4..bbd9fda 100644 --- a/tests/test_observatories.cpp +++ b/tests/test_observatories.cpp @@ -4,36 +4,36 @@ using namespace siderust; TEST(Observatories, RoqueDeLos) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; - // La Palma, approx lon=-17.88, lat=28.76 - EXPECT_NEAR(obs.lon.value(), -17.88, 0.1); - EXPECT_NEAR(obs.lat.value(), 28.76, 0.1); - EXPECT_GT(obs.height.value(), 2000.0); + auto obs = ROQUE_DE_LOS_MUCHACHOS; + // La Palma, approx lon=-17.88, lat=28.76 + EXPECT_NEAR(obs.lon.value(), -17.88, 0.1); + EXPECT_NEAR(obs.lat.value(), 28.76, 0.1); + EXPECT_GT(obs.height.value(), 2000.0); } TEST(Observatories, ElParanal) { - auto obs = EL_PARANAL; - EXPECT_LT(obs.lon.value(), 0.0); - EXPECT_LT(obs.lat.value(), 0.0); // Southern hemisphere - EXPECT_GT(obs.height.value(), 2000.0); + auto obs = EL_PARANAL; + EXPECT_LT(obs.lon.value(), 0.0); + EXPECT_LT(obs.lat.value(), 0.0); // Southern hemisphere + EXPECT_GT(obs.height.value(), 2000.0); } TEST(Observatories, MaunaKea) { - auto obs = MAUNA_KEA; - EXPECT_NEAR(obs.lon.value(), -155.47, 0.1); - EXPECT_NEAR(obs.lat.value(), 19.82, 0.1); - EXPECT_GT(obs.height.value(), 4000.0); + auto obs = MAUNA_KEA; + EXPECT_NEAR(obs.lon.value(), -155.47, 0.1); + EXPECT_NEAR(obs.lat.value(), 19.82, 0.1); + EXPECT_GT(obs.height.value(), 4000.0); } TEST(Observatories, LaSilla) { - auto obs = LA_SILLA_OBSERVATORY; - EXPECT_LT(obs.lon.value(), 0.0); - EXPECT_LT(obs.lat.value(), 0.0); + auto obs = LA_SILLA_OBSERVATORY; + EXPECT_LT(obs.lon.value(), 0.0); + EXPECT_LT(obs.lat.value(), 0.0); } TEST(Observatories, CustomGeodetic) { - auto g = geodetic(-3.7, 40.4, 667.0); - EXPECT_NEAR(g.lon.value(), -3.7, 1e-10); - EXPECT_NEAR(g.lat.value(), 40.4, 1e-10); - EXPECT_NEAR(g.height.value(), 667.0, 1e-10); + auto g = geodetic(-3.7, 40.4, 667.0); + EXPECT_NEAR(g.lon.value(), -3.7, 1e-10); + EXPECT_NEAR(g.lat.value(), 40.4, 1e-10); + EXPECT_NEAR(g.height.value(), 667.0, 1e-10); } diff --git a/tests/test_time.cpp b/tests/test_time.cpp index a2454c3..d88f13a 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -8,36 +8,36 @@ using namespace siderust; // ============================================================================ TEST(Time, JulianDateJ2000) { - auto jd = JulianDate::J2000(); - EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); + auto jd = JulianDate::J2000(); + EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); } TEST(Time, JulianDateFromUtc) { - // UTC noon 2000-01-01 differs from J2000 (TT) by ~64s leap seconds - auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); - EXPECT_NEAR(jd.value(), 2451545.0, 0.001); + // UTC noon 2000-01-01 differs from J2000 (TT) by ~64s leap seconds + auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); + EXPECT_NEAR(jd.value(), 2451545.0, 0.001); } TEST(Time, JulianDateRoundtripUtc) { - UTC original(2026, 7, 15, 22, 0, 0); - auto jd = JulianDate::from_utc(original); - auto utc = jd.to_utc(); - EXPECT_EQ(utc.year, 2026); - EXPECT_EQ(utc.month, 7); - EXPECT_EQ(utc.day, 15); - // Hour may differ slightly due to TT/UTC offset - EXPECT_NEAR(utc.hour, 22, 1); + UTC original(2026, 7, 15, 22, 0, 0); + auto jd = JulianDate::from_utc(original); + auto utc = jd.to_utc(); + EXPECT_EQ(utc.year, 2026); + EXPECT_EQ(utc.month, 7); + EXPECT_EQ(utc.day, 15); + // Hour may differ slightly due to TT/UTC offset + EXPECT_NEAR(utc.hour, 22, 1); } TEST(Time, JulianDateArithmetic) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + qtty::Day(365.25); - EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Day(365.25); + EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); } TEST(Time, JulianCenturies) { - auto jd = JulianDate::J2000(); - EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); + auto jd = JulianDate::J2000(); + EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); } // ============================================================================ @@ -45,16 +45,16 @@ TEST(Time, JulianCenturies) { // ============================================================================ TEST(Time, MjdFromJd) { - auto jd = JulianDate::J2000(); - auto mjd = MJD::from_jd(jd); - EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); + auto jd = JulianDate::J2000(); + auto mjd = MJD::from_jd(jd); + EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); } TEST(Time, MjdRoundtrip) { - auto mjd1 = MJD(60200.0); - auto jd = mjd1.to_jd(); - auto mjd2 = MJD::from_jd(jd); - EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); + auto mjd1 = MJD(60200.0); + auto jd = mjd1.to_jd(); + auto mjd2 = MJD::from_jd(jd); + EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); } // ============================================================================ @@ -62,26 +62,26 @@ TEST(Time, MjdRoundtrip) { // ============================================================================ TEST(Time, PeriodDuration) { - Period p(MJD(60200.0), MJD(60201.0)); - EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } TEST(Time, PeriodIntersection) { - Period a(MJD(60200.0), MJD(60202.0)); - Period b(MJD(60201.0), MJD(60203.0)); - auto c = a.intersection(b); - EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); - EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); + Period a(MJD(60200.0), MJD(60202.0)); + Period b(MJD(60201.0), MJD(60203.0)); + auto c = a.intersection(b); + EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); + EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); } TEST(Time, PeriodNoIntersection) { - Period a(MJD(60200.0), MJD(60201.0)); - Period b(MJD(60202.0), MJD(60203.0)); - EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); + Period a(MJD(60200.0), MJD(60201.0)); + Period b(MJD(60202.0), MJD(60203.0)); + EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); } TEST(Time, PeriodInvalidThrows) { - EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); + EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); } // ============================================================================ @@ -89,47 +89,47 @@ TEST(Time, PeriodInvalidThrows) { // ============================================================================ TEST(Time, JulianCenturiesQty) { - auto jd = JulianDate::J2000(); - auto jc = jd.julian_centuries_qty(); - EXPECT_NEAR(jc.value(), 0.0, 1e-10); - EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); + auto jd = JulianDate::J2000(); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 0.0, 1e-10); + EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); } TEST(Time, JulianCenturiesQtyNonZero) { - // 36525 days ≈ 1 Julian century - auto jd = JulianDate(2451545.0 + 36525.0); - auto jc = jd.julian_centuries_qty(); - EXPECT_NEAR(jc.value(), 1.0, 1e-10); + // 36525 days ≈ 1 Julian century + auto jd = JulianDate(2451545.0 + 36525.0); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 1.0, 1e-10); } TEST(Time, ArithmeticWithHours) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + qtty::Hour(24.0); - EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Hour(24.0); + EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); } TEST(Time, ArithmeticWithMinutes) { - auto mjd1 = MJD(60200.0); - auto mjd2 = mjd1 + qtty::Minute(1440.0); - EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); + auto mjd1 = MJD(60200.0); + auto mjd2 = mjd1 + qtty::Minute(1440.0); + EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); } TEST(Time, SubtractQuantityHours) { - auto jd1 = JulianDate(2451546.0); - auto jd2 = jd1 - qtty::Hour(12.0); - EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); + auto jd1 = JulianDate(2451546.0); + auto jd2 = jd1 - qtty::Hour(12.0); + EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); } TEST(Time, DifferenceConvertible) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = JulianDate(2451546.0); - auto diff = jd2 - jd1; - auto hours = diff.to(); - EXPECT_NEAR(hours.value(), 24.0, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = JulianDate(2451546.0); + auto diff = jd2 - jd1; + auto hours = diff.to(); + EXPECT_NEAR(hours.value(), 24.0, 1e-10); } TEST(Time, PeriodDurationInMinutes) { - Period p(MJD(60200.0), MJD(60200.5)); - auto min = p.duration(); - EXPECT_NEAR(min.value(), 720.0, 1e-6); + Period p(MJD(60200.0), MJD(60200.5)); + auto min = p.duration(); + EXPECT_NEAR(min.value(), 720.0, 1e-6); }