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..8131d86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() # --------------------------------------------------------------------------- @@ -182,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 563c5ff..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 | --- @@ -29,29 +32,27 @@ codebase without writing a single line of Rust. ```cpp #include -#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 at the observatory - 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 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()); + 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::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; } @@ -112,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 @@ -130,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 a3b1a92..f89a497 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -1,140 +1,84 @@ /** * @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 Altitude periods/crossings/culminations for multiple target types. */ -#include - -#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"; -} - -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); -} - -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); - } -} +#include -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); - } +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"; + } } -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; - 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)); - } - if (events.size() > n) { - std::printf(" ... (%zu more)\n", events.size() - n); - } -} +} // namespace 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"); - - // 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); - - 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 new file mode 100644 index 0000000..d17f6a9 --- /dev/null +++ b/examples/azimuth_lunar_phase_example.cpp @@ -0,0 +1,79 @@ +/** + * @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 {} // 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}); + 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) << 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); + + 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); + + 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 << " 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"; + + return 0; +} diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index b7eeb42..3a144e4 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -1,83 +1,47 @@ /** * @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 -using namespace siderust; -using namespace siderust::frames; -using namespace qtty::literals; +#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()); + using namespace siderust; + using namespace siderust::frames; - 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()); + std::cout << "=== coordinate_systems_example ===\n"; - // Vega J2000 ICRS direction. - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + static_assert(has_frame_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_horizontal_transform_v); - 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); + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const auto ecef = observer.to_cartesian(); - 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()); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - 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()); + 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); - 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()); + std::cout << std::fixed << std::setprecision(6); + std::cout << "Observer: " << observer << std::endl; + std::cout << "Observer in ECEF: " << ecef << std::endl; - 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 << "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 f32fa07..ee6fdb8 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -1,101 +1,56 @@ /** * @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 + * @brief Focused typed-coordinate construction and conversion examples. */ -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -static void geodetic_and_ecef_example() { - std::printf("1) Geodetic -> ECEF cartesian\n"); - - 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"); - - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); +#include +#include +#include - auto ecl = vega_icrs.to(jd); - auto eq_mod = vega_icrs.to(jd); - auto hor = vega_icrs.to_horizontal(jd, ROQUE_DE_LOS_MUCHACHOS); +#include - 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()); -} +int main() { + using namespace siderust; + using namespace qtty::literals; -static void spherical_position_example() { - std::printf("3) Spherical position + extracting direction\n"); + std::cout << "=== coordinates_examples ===\n"; - spherical::position::ICRS target( - 120.0_deg, -25.0_deg, 2.0e17_m); - auto dir = target.direction(); + 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(); - 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_assert(std::is_same_v, + cartesian::position::ECEF>); -static void cartesian_and_units_example() { - std::printf("4) Cartesian coordinate creation + unit conversion\n"); + std::cout << "Geodetic -> ECEF \n " << site << "\n" + << ecef_m << "\n" + << "(" << ecef_km << ")\n" + << std::endl; - cartesian::Direction axis_x(1.0, 0.0, 0.0); - cartesian::position::EclipticMeanJ2000 sample_helio_au(1.0, 0.25, -0.1); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto x_km = sample_helio_au.x().to(); - auto y_km = sample_helio_au.y().to(); + 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::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()); -} + 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; -static void ephemeris_typed_example() { - std::printf("5) Typed ephemeris coordinates\n"); + spherical::position::ICRS synthetic_star( + 210.0_deg, -12.0_deg, 4.2_au); - 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"); + cartesian::position::EclipticMeanJ2000 earth = + ephemeris::earth_heliocentric(jd); - geodetic_and_ecef_example(); - spherical_direction_example(); - spherical_position_example(); - cartesian_and_units_example(); - ephemeris_typed_example(); + 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::printf("Done.\n"); - return 0; + return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 40c2038..a0e8c23 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -1,125 +1,101 @@ /** * @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 {} // namespace + int main() { - using namespace siderust; - using namespace siderust::frames; - using namespace qtty::literals; - - std::printf("=== siderust-cpp demo ===\n\n"); - - // --- 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()); - - auto mjd = MJD::from_jd(jd); - std::printf("MJD: %.6f\n\n", mjd.value()); - - // --- 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 --- - 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 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()); - - 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); - } - - std::printf("\nDone.\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 bf82f2f..25c69d7 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -1,79 +1,81 @@ /** * @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 -using namespace siderust; +#include -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); -} +namespace { -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); +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() { - std::printf("=== Solar System Bodies Example ===\n\n"); + 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)); + + std::cout << "=== solar_system_bodies_example ===\n"; + std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; - auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::printf("Epoch JD: %.6f\n\n", jd.value()); + 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 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); + 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::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()); + 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"; - 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::vector tracked = {Body::Sun, Body::Moon, Body::Mars, + Body::Jupiter}; - const qtty::Kilometer earth_x_km = earth_helio.x().to(); - std::printf("Earth heliocentric x component: %.2f km\n\n", earth_x_km.value()); + 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::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); + 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 new file mode 100644 index 0000000..2e578be --- /dev/null +++ b/examples/trackable_targets_example.cpp @@ -0,0 +1,92 @@ +/** + * @file trackable_targets_example.cpp + * @example trackable_targets_example.cpp + * @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 +#include +#include +#include +#include +#include + +#include + +namespace { + +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"; + + // 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"; + } + } + + 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 new file mode 100644 index 0000000..37640bd --- /dev/null +++ b/include/siderust/azimuth.hpp @@ -0,0 +1,419 @@ +#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 +#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); +} + +/** + * @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 + +// ============================================================================ +// 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/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 new file mode 100644 index 0000000..855bafd --- /dev/null +++ b/include/siderust/body_target.hpp @@ -0,0 +1,300 @@ +#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/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 79a069b..2a681b2 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -12,6 +12,8 @@ #include +#include + namespace siderust { namespace cartesian { @@ -23,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; + } }; /** @@ -49,42 +50,56 @@ 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; } +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); + } +}; - /// 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()}; - } +// ============================================================================ +// Stream operators +// ============================================================================ - /// 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); - } -}; +/** + * @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/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 86dc552..32c5e69 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -12,10 +12,11 @@ #include +#include + namespace siderust { namespace cartesian { -template -struct Position; +template struct Position; } /** @@ -26,38 +27,50 @@ 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. - - 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) {} - - /// 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()}; - } - - /// 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; + 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(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)) {} + + /// 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); + } + + /** + * @brief Convert geodetic (WGS84/ECEF) to cartesian position. + * + * @tparam U Output length unit (default: meter). + */ + template + 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 cd94da9..1053eb7 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,9 @@ 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: @@ -32,138 +35,157 @@ namespace spherical { * - 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) {} - - /// 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() { - 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 {azimuth_.value(), polar_.value(), frame_id()}; - } - - static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.lon_deg, c.lat_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_.value(), polar_.value()); - } else { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_transform_frame( - azimuth_.value(), polar_.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( - azimuth_.value(), polar_.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); + } }; /** @@ -176,62 +198,107 @@ 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) {} - - 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_); - } - - /// @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 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 , int> = 0> - qtty::Degree lon() const { return azimuth_; } +// ============================================================================ +// Stream operators +// ============================================================================ - template , int> = 0> - qtty::Degree lat() const { return polar_; } - /// @} +/** + * @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(); +} - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } +/** + * @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(); +} - U distance() const { return dist_; } -}; +/** + * @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/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 94d8a9f..df3c5b2 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -9,6 +9,7 @@ */ #include +#include #include #include @@ -25,136 +26,178 @@ 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); } +// ============================================================================ +// 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) // ============================================================================ 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, 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 new file mode 100644 index 0000000..1db1aa6 --- /dev/null +++ b/include/siderust/lunar_phase.hpp @@ -0,0 +1,355 @@ +#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 +#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 + +// ============================================================================ +// 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; + } +} + +// ============================================================================ +// 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/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 5cf0df1..63e0c05 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,25 +13,33 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(279.23473, 38.78369); // Direction - * 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 */ #include "altitude.hpp" +#include "azimuth.hpp" #include "bodies.hpp" +#include "body_target.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 "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..40f0a02 --- /dev/null +++ b/include/siderust/star_target.hpp @@ -0,0 +1,98 @@ +#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 new file mode 100644 index 0000000..af7fba3 --- /dev/null +++ b/include/siderust/target.hpp @@ -0,0 +1,406 @@ +#pragma once + +/** + * @file target.hpp + * @brief Generic strongly-typed RAII wrapper for a siderust Target direction. + * + * `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. + * + * 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" +#include "azimuth.hpp" +#include "coordinates.hpp" +#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 wrapper for a fixed celestial target direction. + * + * @tparam C Spherical direction type (e.g. `spherical::direction::ICRS`). + * + * ### Example — ICRS target (Vega at J2000) + * @code + * 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 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; + } + + // 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; + } +}; + +// ============================================================================ +// 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/include/siderust/time.hpp b/include/siderust/time.hpp index 351af5a..126f781 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/include/siderust/trackable.hpp b/include/siderust/trackable.hpp new file mode 100644 index 0000000..08a236f --- /dev/null +++ b/include/siderust/trackable.hpp @@ -0,0 +1,125 @@ +#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/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..17d986f 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 283032a541a8bbcc46622e6a06f807717438f998 +Subproject commit 17d986fd5d1c9c258df50ce66f36fc524266d6e4 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/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 8ad505a..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 + 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_days(), 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_days(), 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_days(), 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_days(), 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,15 +114,83 @@ 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); +} + +// ============================================================================ +// 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_bodies.cpp b/tests/test_bodies.cpp index 755e69b..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,26 +59,149 @@ 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); +} + +// ============================================================================ +// 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())); + } } diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 8cc6cc7..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(279.23473, 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(100.0, 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(100.0, 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(279.23473, 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(100.0, 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(100.0, 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(123.456, -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(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(); - 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 395814c..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 + 365.25; - EXPECT_NEAR(jd2 - jd1, 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,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); - auto c = a.intersection(b); - EXPECT_NEAR(c.start_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(c.end_mjd(), 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(60200.0, 60201.0); - Period b(60202.0, 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(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); }