From b02680a36014caea27a74b398264ca837b78e483 Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Tue, 22 Apr 2025 20:47:17 +0200 Subject: [PATCH] Add new TimeFrame that parses Spice kernels (#3600) --- .../{ => timeframeinterval}/interval.asset | 0 .../timeframekernel/kernel_ck-multiple.asset | 44 ++ .../timeframe/timeframekernel/kernel_ck.asset | 42 ++ .../timeframekernel/kernel_spk-ck.asset | 48 ++ .../timeframekernel/kernel_spk-multiple.asset | 42 ++ .../timeframekernel/kernel_spk.asset | 40 ++ .../{ => timeframeunion}/union.asset | 0 include/openspace/util/spicemanager.h | 7 + modules/base/timeframe/timeframeunion.cpp | 4 +- modules/space/CMakeLists.txt | 2 + modules/space/spacemodule.cpp | 9 + modules/space/timeframe/timeframekernel.cpp | 485 ++++++++++++++++++ modules/space/timeframe/timeframekernel.h | 52 ++ src/util/spicemanager.cpp | 9 +- support/coding/codegen | 2 +- 15 files changed, 781 insertions(+), 5 deletions(-) rename data/assets/examples/timeframe/{ => timeframeinterval}/interval.asset (100%) create mode 100644 data/assets/examples/timeframe/timeframekernel/kernel_ck-multiple.asset create mode 100644 data/assets/examples/timeframe/timeframekernel/kernel_ck.asset create mode 100644 data/assets/examples/timeframe/timeframekernel/kernel_spk-ck.asset create mode 100644 data/assets/examples/timeframe/timeframekernel/kernel_spk-multiple.asset create mode 100644 data/assets/examples/timeframe/timeframekernel/kernel_spk.asset rename data/assets/examples/timeframe/{ => timeframeunion}/union.asset (100%) create mode 100644 modules/space/timeframe/timeframekernel.cpp create mode 100644 modules/space/timeframe/timeframekernel.h diff --git a/data/assets/examples/timeframe/interval.asset b/data/assets/examples/timeframe/timeframeinterval/interval.asset similarity index 100% rename from data/assets/examples/timeframe/interval.asset rename to data/assets/examples/timeframe/timeframeinterval/interval.asset diff --git a/data/assets/examples/timeframe/timeframekernel/kernel_ck-multiple.asset b/data/assets/examples/timeframe/timeframekernel/kernel_ck-multiple.asset new file mode 100644 index 0000000000..c8e27a403b --- /dev/null +++ b/data/assets/examples/timeframe/timeframekernel/kernel_ck-multiple.asset @@ -0,0 +1,44 @@ +-- CK Multiple +-- This example creates a time frame based on the information provided by multiple SPICE +-- kernel files that contain orientation information about the same object. The created +-- scene graph node will only be valid whenever any window in any of the provided kernels +-- contains information about refernence frame "-98000", which is the intertial +-- orientation frame for the New Horizons spacecraft. + +-- We need a SPICE kernel to work with in this example +local data = asset.resource({ + Name = "New Horizons Kernels", + Type = "HttpSynchronization", + Identifier = "newhorizons_kernels", + Version = 1 +}) + +local Node = { + Identifier = "TimeFrameKernel_Example_CK_Multiple", + TimeFrame = { + Type = "TimeFrameKernel", + CK = { + Kernels = { + data .. "nh_apf_20150404_20150420_001.bc", + data .. "nh_apf_20150420_20150504_001.bc", + data .. "new-horizons_1121.tsc" + }, + Reference = "-98000" + } + }, + Renderable = { + Type = "RenderableCartesianAxes" + }, + GUI = { + Name = "TimeFrameKernel - Basic (CK, Multiple)", + Path = "/Examples" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(Node) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Node) +end) diff --git a/data/assets/examples/timeframe/timeframekernel/kernel_ck.asset b/data/assets/examples/timeframe/timeframekernel/kernel_ck.asset new file mode 100644 index 0000000000..c9354830e9 --- /dev/null +++ b/data/assets/examples/timeframe/timeframekernel/kernel_ck.asset @@ -0,0 +1,42 @@ +-- CK Basic +-- This example creates a time frame based on the information provided by a single SPICE +-- kernel file. The created scene graph node will only be valid whenever the provided +-- kernel contains information about about the reference frame "-98000", which is the +-- interial orientation frame for the New Horizons spacecraft. + +-- We need a SPICE kernel to work with in this example +local data = asset.resource({ + Name = "New Horizons Kernels", + Type = "HttpSynchronization", + Identifier = "newhorizons_kernels", + Version = 1 +}) + +local Node = { + Identifier = "TimeFrameKernel_Example_CK", + TimeFrame = { + Type = "TimeFrameKernel", + CK = { + Kernels = { + data .. "nh_apf_20150404_20150420_001.bc", + data .. "new-horizons_1121.tsc", + }, + Reference = "-98000" + } + }, + Renderable = { + Type = "RenderableCartesianAxes" + }, + GUI = { + Name = "TimeFrameKernel - Basic (CK)", + Path = "/Examples" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(Node) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Node) +end) diff --git a/data/assets/examples/timeframe/timeframekernel/kernel_spk-ck.asset b/data/assets/examples/timeframe/timeframekernel/kernel_spk-ck.asset new file mode 100644 index 0000000000..87547dc032 --- /dev/null +++ b/data/assets/examples/timeframe/timeframekernel/kernel_spk-ck.asset @@ -0,0 +1,48 @@ +-- Combined example +-- This example creates a time frame based on the information provided by multiple SPICE +-- kernel files. The created scene graph node will only be valid whenever the provided +-- kernels contain information positional information about the object "JUICE" as well as +-- orientation information for the reference frame "-28002" which is the measured attitude +-- for the JUICE spacecraft. The time frame will only be valid if both pieces of data are +-- available. + +-- We need a SPICE kernel to work with in this example +local data = asset.resource({ + Name = "JUICE Kernels", + Type = "HttpSynchronization", + Identifier = "juice_kernels", + Version = 2 +}) + +local Node = { + Identifier = "TimeFrameKernel_Example_Combined_SPK-CK", + TimeFrame = { + Type = "TimeFrameKernel", + SPK = { + Kernels = data .. "juice_orbc_000031_230414_310721_v03.bsp", + Object = "JUICE" + }, + CK = { + Kernels = { + data .. "juice_sc_meas_230413_230415_s230414_v01.bc", + data .. "juice_step_230414_v01.tsc" + }, + Reference = "-28002" + } + }, + Renderable = { + Type = "RenderableCartesianAxes" + }, + GUI = { + Name = "TimeFrameKernel - Combined (SPK+CK)", + Path = "/Examples" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(Node) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Node) +end) diff --git a/data/assets/examples/timeframe/timeframekernel/kernel_spk-multiple.asset b/data/assets/examples/timeframe/timeframekernel/kernel_spk-multiple.asset new file mode 100644 index 0000000000..9425d231ad --- /dev/null +++ b/data/assets/examples/timeframe/timeframekernel/kernel_spk-multiple.asset @@ -0,0 +1,42 @@ +-- SPK Multiple +-- This example creates a time frame based on the information provided by multiple SPICE +-- kernel files that contain position information about the same object. The created scene +-- graph node will only be valid whenever any window in any of the provided kernels +-- contains information about object "VOYAGER 1". + +-- We need a SPICE kernel to work with in this example +local data = asset.resource({ + Name = "Voyager 1 Kernels", + Type = "HttpSynchronization", + Identifier = "voyager1_spice", + Version = 2 +}) + +local Node = { + Identifier = "TimeFrameKernel_Example_SPK_Multiple", + TimeFrame = { + Type = "TimeFrameKernel", + SPK = { + Kernels = { + data .. "vgr1_jup230.bsp", + data .. "vgr1_sat337.bsp" + }, + Object = "VOYAGER 1" + } + }, + Renderable = { + Type = "RenderableCartesianAxes" + }, + GUI = { + Name = "TimeFrameKernel - Basic (SPK, Multiple)", + Path = "/Examples" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(Node) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Node) +end) diff --git a/data/assets/examples/timeframe/timeframekernel/kernel_spk.asset b/data/assets/examples/timeframe/timeframekernel/kernel_spk.asset new file mode 100644 index 0000000000..f77fccf4e7 --- /dev/null +++ b/data/assets/examples/timeframe/timeframekernel/kernel_spk.asset @@ -0,0 +1,40 @@ +-- SPK Basic +-- This example creates a time frame based on the information provided by a single SPICE +-- kernel file. The created scene graph node will only be valid whenever the provided +-- kernel contains information about object "-915", which is Apollo 15. In this specific +-- case, the Apollo15 kernel contains two windows of valid data, both of which are used by +-- this time frame. + +-- We need a SPICE kernel to work with in this example +local data = asset.resource({ + Name = "Apollo Kernels", + Type = "HttpSynchronization", + Identifier = "apollo_spice", + Version = 1 +}) + +local Node = { + Identifier = "TimeFrameKernel_Example_SPK", + TimeFrame = { + Type = "TimeFrameKernel", + SPK = { + Kernels = data .. "apollo15-1.bsp", + Object = "-915" + } + }, + Renderable = { + Type = "RenderableCartesianAxes" + }, + GUI = { + Name = "TimeFrameKernel - Basic (SPK)", + Path = "/Examples" + } +} + +asset.onInitialize(function() + openspace.addSceneGraphNode(Node) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Node) +end) diff --git a/data/assets/examples/timeframe/union.asset b/data/assets/examples/timeframe/timeframeunion/union.asset similarity index 100% rename from data/assets/examples/timeframe/union.asset rename to data/assets/examples/timeframe/timeframeunion/union.asset diff --git a/include/openspace/util/spicemanager.h b/include/openspace/util/spicemanager.h index 20e55d0189..69c1197fbc 100644 --- a/include/openspace/util/spicemanager.h +++ b/include/openspace/util/spicemanager.h @@ -1029,6 +1029,13 @@ public: */ UseException exceptionHandling() const; + /** + * Returns the path to the most current leap second kernel. + * + * \return The path to the most current leap second kernel. + */ + static std::filesystem::path leapSecondKernel(); + static scripting::LuaLibrary luaLibrary(); private: diff --git a/modules/base/timeframe/timeframeunion.cpp b/modules/base/timeframe/timeframeunion.cpp index 285096d850..5be70a6bae 100644 --- a/modules/base/timeframe/timeframeunion.cpp +++ b/modules/base/timeframe/timeframeunion.cpp @@ -40,8 +40,8 @@ namespace { openspace::properties::Property::Visibility::AdvancedUser }; - // This TimeFrame class will accept the union of all passed-in TimeFrames. This means - // that this TimeFrame will be active if at least one of the child TimeFrames is + // This `TimeFrame` class will accept the union of all passed-in TimeFrames. This + // means that this TimeFrame will be active if at least one of the child TimeFrames is // active and it will be inactive if none of the child TimeFrames are active. // // This can be used to create more complex TimeFrames that are made up of several, diff --git a/modules/space/CMakeLists.txt b/modules/space/CMakeLists.txt index 15cedfd993..45528b42b3 100644 --- a/modules/space/CMakeLists.txt +++ b/modules/space/CMakeLists.txt @@ -37,6 +37,7 @@ set(HEADER_FILES rendering/renderableorbitalkepler.h rendering/renderablestars.h rendering/renderabletravelspeed.h + timeframe/timeframekernel.h translation/gptranslation.h translation/keplertranslation.h translation/spicetranslation.h @@ -59,6 +60,7 @@ set(SOURCE_FILES rendering/renderableorbitalkepler.cpp rendering/renderablestars.cpp rendering/renderabletravelspeed.cpp + timeframe/timeframekernel.cpp translation/gptranslation.cpp translation/keplertranslation.cpp translation/spicetranslation.cpp diff --git a/modules/space/spacemodule.cpp b/modules/space/spacemodule.cpp index 8a6939d61f..fc08350ef9 100644 --- a/modules/space/spacemodule.cpp +++ b/modules/space/spacemodule.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +106,7 @@ void SpaceModule::internalInitialize(const ghoul::Dictionary& dictionary) { fRenderable->registerClass("RenderableStars"); fRenderable->registerClass("RenderableTravelSpeed"); + ghoul::TemplateFactory* fTranslation = FactoryManager::ref().factory(); ghoul_assert(fTranslation, "Ephemeris factory was not created"); @@ -114,12 +116,19 @@ void SpaceModule::internalInitialize(const ghoul::Dictionary& dictionary) { fTranslation->registerClass("GPTranslation"); fTranslation->registerClass("HorizonsTranslation"); + ghoul::TemplateFactory* fRotation = FactoryManager::ref().factory(); ghoul_assert(fRotation, "Rotation factory was not created"); fRotation->registerClass("SpiceRotation"); + + ghoul::TemplateFactory* fTimeFrame = + FactoryManager::ref().factory(); + ghoul_assert(fTimeFrame, "Scale factory was not created"); + fTimeFrame->registerClass("TimeFrameKernel"); + const Parameters p = codegen::bake(dictionary); _showSpiceExceptions = p.showExceptions.value_or(_showSpiceExceptions); } diff --git a/modules/space/timeframe/timeframekernel.cpp b/modules/space/timeframe/timeframekernel.cpp new file mode 100644 index 0000000000..cf12dc09cf --- /dev/null +++ b/modules/space/timeframe/timeframekernel.cpp @@ -0,0 +1,485 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include "SpiceUsr.h" + +namespace { + constexpr std::string_view _loggerCat = "TimeFrameKernel"; + constexpr unsigned SpiceErrorBufferSize = 1841; + + std::vector extractTimeFramesSPK( + const std::vector& kernels, + const std::variant& object) + { + using namespace openspace; + std::vector res; + + // Load the kernel to be able to resolve the provided object name + const std::filesystem::path currentDirectory = std::filesystem::current_path(); + for (const std::filesystem::path& kernel : kernels) { + const std::filesystem::path p = kernel.parent_path(); + std::filesystem::current_path(p); + const std::string k = kernel.string(); + furnsh_c(k.c_str()); + } + + // Convert the provided object name into a SpiceInt + SpiceBoolean success = SPICEFALSE; + SpiceInt id = 0; + if (std::holds_alternative(object)) { + std::string s = std::get(object); + bods2c_c(s.c_str(), &id, &success); + if (!success) { + throw ghoul::RuntimeError(std::format("Error finding object '{}'", s)); + } + } + else { + ghoul_assert(std::holds_alternative(object), "Additional variant type"); + id = std::get(object); + } + + + // Set up variables + constexpr unsigned int MaxObj = 1024; + SPICEINT_CELL(ids, MaxObj); + constexpr unsigned int WinSiz = 16384; + SPICEDOUBLE_CELL(cover, WinSiz); + scard_c(0, &cover); + + // Get all objects in the provided kernels + for (const std::filesystem::path& kernel : kernels) { + const std::string k = kernel.string(); + + constexpr int ArchitectureSize = 128; + std::array architecture; + std::memset(architecture.data(), ArchitectureSize, 0); + + constexpr int TypeSize = 16; + std::array type; + std::memset(type.data(), TypeSize, 0); + + getfat_c( + k.c_str(), + ArchitectureSize, + TypeSize, + architecture.data(), + type.data() + ); + + if (std::string_view(type.data()) != "SPK") { + // Only SPK kernels are allowed, but we want the user to be able to pass + // the full list of kernels to this class, which includes other types + continue; + } + + spkobj_c(k.c_str(), &ids); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error loading kernel {}. {}", kernel, buffer + )); + } + for (SpiceInt i = 0; i < card_c(&ids); i++) { + const SpiceInt obj = SPICE_CELL_ELEM_I(&ids, i); + + if (obj != id) { + // We only want to find the coverage for the specific identifier + continue; + } + + // Get coverage for the object + spkcov_c(k.c_str(), obj, &cover); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error finding SPK coverage '{}'. {}", kernel, buffer + )); + } + + // Access all of the windows + const SpiceInt numberOfIntervals = wncard_c(&cover); + for (SpiceInt j = 0; j < numberOfIntervals; j++) { + // Get the endpoints of the jth interval + SpiceDouble b = 0.0; + SpiceDouble e = 0.0; + wnfetd_c(&cover, j, &b, &e); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error finding window {} in SPK '{}'. {}", j, kernel, buffer + )); + } + + res.emplace_back(b, e); + } + } + } + + + // We no longer need to have need for the kernel being loaded + for (const std::filesystem::path& kernel : kernels) { + const std::filesystem::path p = kernel.parent_path(); + std::filesystem::current_path(p); + const std::string k = kernel.string(); + unload_c(k.c_str()); + } + std::filesystem::current_path(currentDirectory); + + + return res; + } + + std::vector extractTimeFramesCK( + const std::vector& kernels, + const std::variant& object) + { + using namespace openspace; + std::vector res; + + std::filesystem::path lsk = SpiceManager::leapSecondKernel(); + const std::string l = lsk.string(); + furnsh_c(l.c_str()); + + // Load the kernel to be able to resolve the provided object name + const std::filesystem::path currentDirectory = std::filesystem::current_path(); + + for (const std::filesystem::path& kernel : kernels) { + const std::filesystem::path p = kernel.parent_path(); + std::filesystem::current_path(p); + const std::string k = kernel.string(); + furnsh_c(k.c_str()); + } + + // Convert the provided reference name into a SpiceInt + SpiceBoolean success = SPICEFALSE; + SpiceInt id = 0; + if (std::holds_alternative(object)) { + std::string s = std::get(object); + bods2c_c(s.c_str(), &id, &success); + if (!success) { + throw ghoul::RuntimeError(std::format("Error finding object '{}'", s)); + } + } + else { + ghoul_assert(std::holds_alternative(object), "Additional variant type"); + id = std::get(object); + } + + + // Set up variables + constexpr unsigned int MaxObj = 1024; + SPICEINT_CELL(ids, MaxObj); + constexpr unsigned int WinSiz = 16384; + SPICEDOUBLE_CELL(cover, WinSiz); + scard_c(0, &cover); + + // Get all objects in the provided kernel + for (const std::filesystem::path& kernel : kernels) { + const std::string k = kernel.string(); + + constexpr int ArchitectureSize = 128; + std::array architecture; + std::memset(architecture.data(), ArchitectureSize, 0); + + constexpr int TypeSize = 16; + std::array type; + std::memset(type.data(), TypeSize, 0); + + getfat_c( + k.c_str(), + ArchitectureSize, + TypeSize, + architecture.data(), + type.data() + ); + + if (std::string_view(type.data()) != "CK") { + // Since SCLK kernels are allowed as well we can't throw an exception + // here. We can't even warn about it since the tested spacecraft clock + // kernels report a type and architecture of '?' which is not helpful. + continue; + } + + ckobj_c(k.c_str(), &ids); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error loading kernel {}. {}", kernel, buffer + )); + } + for (SpiceInt i = 0; i < card_c(&ids); i++) { + const SpiceInt frame = SPICE_CELL_ELEM_I(&ids, i); + + if (frame != id) { + // We only want to find the coverage for the specific identifier + continue; + } + + // Get coverage for the object + ckcov_c(k.c_str(), frame, SPICEFALSE, "SEGMENT", 0.0, "TDB", &cover); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error finding CK coverage '{}'. {}", kernel, buffer + )); + } + + // Access all of the windows + const SpiceInt numberOfIntervals = wncard_c(&cover); + for (SpiceInt j = 0; j < numberOfIntervals; j++) { + // Get the endpoints of the jth interval + SpiceDouble b = 0.0; + SpiceDouble e = 0.0; + wnfetd_c(&cover, j, &b, &e); + if (failed_c()) { + std::string buffer; + buffer.resize(SpiceErrorBufferSize); + getmsg_c("LONG", SpiceErrorBufferSize, buffer.data()); + reset_c(); + throw ghoul::RuntimeError(std::format( + "Error finding window {} in SPK '{}'. {}", j, kernel, buffer + )); + } + + res.emplace_back(b, e); + } + } + } + + // We no longer need to have need for the kernel being loaded + for (const std::filesystem::path& kernel : kernels) { + const std::filesystem::path p = kernel.parent_path(); + std::filesystem::current_path(p); + const std::string k = kernel.string(); + unload_c(k.c_str()); + } + unload_c(l.c_str()); + std::filesystem::current_path(currentDirectory); + + + return res; + } + + void normalizeTimeRanges(std::vector& ranges) { + using namespace openspace; + + if (ranges.size() <= 1) { + // Nothing to do here if there is 0 or 1 elements in the vector + return; + } + + // 1. Sort time frames based on their beginning time. If the beginning times are + // the same, sort by the end date instead + std::sort( + ranges.begin(), + ranges.end(), + [](const TimeRange& lhs, const TimeRange& rhs) { + return lhs.start == rhs.start ? lhs.end < rhs.end : lhs.start < lhs.start; + } + ); + + // 2. If `i`'s end time is after `i+1`'s begin time, we can merge these two + ghoul_assert(ranges.size() > 1, "Too few items. Possible underflow"); + for (size_t i = 0; i < ranges.size() - 1; i++) { + TimeRange& curr = ranges[i]; + TimeRange& next = ranges[i + 1]; + + if (curr.end >= next.start) { + // Include the next with the current + curr.include(next); + + // Remove the next as we have subsumed it + ranges.erase(ranges.begin() + i + 1); + } + } + } + + // This `TimeFrame` class determines its time ranges based on the set of provided + // SPICE kernels. Any number of SPK (for position information) or CK (for orientation + // information) kernels can be specified together with a SPICE object name (for + // position information) or the name of a valid reference frame (for orientation + // information). For more information about Spice kernels, windows, or IDs, see the + // required reading documentation from NAIF: + // - https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/kernel.html + // - https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/spk.html + // - https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/ck.html + // - https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/time.html + // - https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/windows.html + // + // Note that for the CK kernels, a valid CK kernel as well as the corresponding SCLK + // kernel must be provided as the latter is required to be able to interpret the time + // codes of the former. For this reason it is not possible to provide only a single + // kernel to the CK struct in this class. + // + // The resulting validity of the time frame is based on the following conditions: + // + // 1. If either SPK or CK (but not both) are specified, the time frame depends on + // the union of all windows within all kernels that were provided. This means + // that if the simulation time is within any time where the kernel has data for + // the provided object, the TimeFrame will be valid. + // 2. If SPK and CK kernels are both specified, the time range validity for SPK and + // CK kernels are calculated separately, but both results must be valid to result + // in a valid time frame. This means that if only position data is available but + // not orientation data, the time frame is invalid. Only if positional and + // orientation data is available, then the TimeFrame will be valid. + // 3. If neither SPK nor CK kernels are specified, the creation of the `TimeFrame` + // will fail. + struct [[codegen::Dictionary(TimeFrameKernel)]] Parameters { + // Specifies information about the kernels and object name used to extract the + // times when positional information for the provided object is available. + struct SPK { + // The path to the kernel or list of kernels that should be loaded to extract + // the positional information. At least one kernel must be a SPK-type kernel. + // Any other kernel type is ignored. + std::variant< + std::filesystem::path, std::vector + > kernels; + + // The NAIF name of the object for which the positional information should be + // extracted + std::variant object; + }; + std::optional spk [[codegen::key("SPK")]]; + + // Specifies information about the kernels and refrence frame name used to extract + // the times when positional information for the provided object is available. + struct CK { + // The path to the list of kernels that should be loaded to extract + // orientation information. At least one kernel must be a CK-type kernel and + // if needed, a SCLK (spacecraft clock) kernel musat be provided. Any other + // kernel type is ignored. + std::vector kernels; + + // The NAIF name of the reference frame for which the times are extacted at + // which this reference frame has data in the provided kernels + std::variant reference; + }; + std::optional ck [[codegen::key("CK")]]; + }; +#include "timeframekernel_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation TimeFrameKernel::Documentation() { + return codegen::doc("space_time_frame_kernel"); +} + +TimeFrameKernel::TimeFrameKernel(const ghoul::Dictionary& dictionary) + : _initialization(dictionary) +{ + // Baking the dictionary here to detect any error + codegen::bake(dictionary); +} + +bool TimeFrameKernel::initialize() { + const Parameters p = codegen::bake(_initialization); + + // Either the SPK or the CK variable must be specified + if (!p.spk.has_value() && !p.ck.has_value()) { + throw ghoul::RuntimeError( + "Either the SPK or the CK (or both) values must be specified for the " + "TimeFrameKernel. Neither was specified." + ); + } + + + // Extract the SPK file/files if they were specified + if (p.spk.has_value()) { + std::vector kernels; + if (std::holds_alternative(p.spk->kernels)) { + kernels = { std::get(p.spk->kernels) }; + } + else { + kernels = std::get>(p.spk->kernels); + } + + _timeRangesSPK = extractTimeFramesSPK(kernels, p.spk->object); + LDEBUG(std::format("Extracted {} SPK time ranges", _timeRangesSPK.size())); + } + + // Extract the CK file/files if they were specified + if (p.ck.has_value()) { + _timeRangesCK = extractTimeFramesCK(p.ck->kernels, p.ck->reference); + LDEBUG(std::format("Extracted {} CK time ranges", _timeRangesCK.size())); + } + + // + // Normalize the timeframes to simplify them as much as possible to reduce the length + // of the vector and improve performance in the `update` lookup + normalizeTimeRanges(_timeRangesSPK); + normalizeTimeRanges(_timeRangesCK); + + return true; +} + +void TimeFrameKernel::update(const Time& time) { + // We don't set _isInTimeFrame directly here as that would trigger an invalidation of + // the property and cause a data transmission every frame. This way, the data is only + // sent if the value actually changes, which should be rare + const double t = time.j2000Seconds(); + bool isInTimeFrameSPK = false; + if (_timeRangesSPK.empty()) { + isInTimeFrameSPK = true; + } + for (const TimeRange& range : _timeRangesSPK) { + if (range.includes(t)) { + isInTimeFrameSPK = true; + break; + } + } + bool isInTimeFrameCK = false; + if (_timeRangesCK.empty()) { + isInTimeFrameCK = true; + } + for (const TimeRange& range : _timeRangesCK) { + if (range.includes(t)) { + isInTimeFrameCK = true; + break; + } + } + _isInTimeFrame = isInTimeFrameSPK && isInTimeFrameCK; +} + +} // namespace diff --git a/modules/space/timeframe/timeframekernel.h b/modules/space/timeframe/timeframekernel.h new file mode 100644 index 0000000000..568325f338 --- /dev/null +++ b/modules/space/timeframe/timeframekernel.h @@ -0,0 +1,52 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_BASE___TIMEFRAMEKERNEL___H__ +#define __OPENSPACE_MODULE_BASE___TIMEFRAMEKERNEL___H__ + +#include + +#include + +namespace openspace { + +class TimeFrameKernel : public TimeFrame { +public: + explicit TimeFrameKernel(const ghoul::Dictionary& dictionary); + + bool initialize() override; + void update(const Time& time) override; + + static documentation::Documentation Documentation(); + +private: + ghoul::Dictionary _initialization; + + std::vector _timeRangesSPK; + std::vector _timeRangesCK; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___TIMEFRAMEKERNEL___H__ diff --git a/src/util/spicemanager.cpp b/src/util/spicemanager.cpp index 74652d9585..b53fa40b9c 100644 --- a/src/util/spicemanager.cpp +++ b/src/util/spicemanager.cpp @@ -1388,7 +1388,7 @@ glm::dmat3 SpiceManager::getEstimatedTransformMatrix(const std::string& fromFram return result; } -void SpiceManager::loadLeapSecondsSpiceKernel() { +std::filesystem::path SpiceManager::leapSecondKernel() { constexpr std::string_view Naif00012tlsSource = R"(KPL/LSK @@ -1542,12 +1542,17 @@ DELTET/DELTA_AT = ( 10, @1972-JAN-1 )"; -const std::filesystem::path path = std::filesystem::temp_directory_path(); + const std::filesystem::path path = std::filesystem::temp_directory_path(); const std::filesystem::path file = path / "naif0012.tls"; { std::ofstream f(file); f << Naif00012tlsSource; } + return file; +} + +void SpiceManager::loadLeapSecondsSpiceKernel() { + std::filesystem::path file = leapSecondKernel(); loadKernel(file); } diff --git a/support/coding/codegen b/support/coding/codegen index 3d3cb4e0c0..664bb14e12 160000 --- a/support/coding/codegen +++ b/support/coding/codegen @@ -1 +1 @@ -Subproject commit 3d3cb4e0c00d82f886531ffe9698961d0104a88d +Subproject commit 664bb14e12335b0d5ea166f633fd571859fdd5c2