diff --git a/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/data/example.json b/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/data/example.json new file mode 100644 index 0000000000..228e23d59d --- /dev/null +++ b/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/data/example.json @@ -0,0 +1,16 @@ +{ + "files": [ + { + "timestamp": "2024-05-10 15:00:00.000", + "url": "http://data.openspaceproject.com/examples/renderableplaneimageonline.jpg" + }, + { + "timestamp": "2024-05-10 15:10:00.000", + "url": "https://data.openspaceproject.com/examples/renderableplaneimageonline_2.jpg" + }, + { + "timestamp": "2024-05-10 15:20:00.000", + "url": "http://data.openspaceproject.com/examples/renderableplaneimageonline.jpg" + } + ] +} diff --git a/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/timevaryingimageonline.asset b/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/timevaryingimageonline.asset new file mode 100644 index 0000000000..8feef5dbb7 --- /dev/null +++ b/data/assets/examples/screenspacerenderable/screenspacetimevaryingimageonline/timevaryingimageonline.asset @@ -0,0 +1,18 @@ +-- Basic +-- Create a time-varying screenspace image plane that shows the content of images from +-- web URLs based on in-game simulation time. The data in this example has images shown at +-- 2024-05-10 between 15:00:00 and 15:20:00. + +local Item = { + Type = "ScreenSpaceTimeVaryingImageOnline", + Identifier = "ScreenSpaceTimeVaryingImageOnline_Example", + FilePath = asset.resource("data/example.json") +} + +asset.onInitialize(function() + openspace.addScreenSpaceRenderable(Item) +end) + +asset.onDeinitialize(function() + openspace.removeScreenSpaceRenderable(Item) +end) diff --git a/modules/base/CMakeLists.txt b/modules/base/CMakeLists.txt index d2f82ed79f..995f25299b 100644 --- a/modules/base/CMakeLists.txt +++ b/modules/base/CMakeLists.txt @@ -75,6 +75,7 @@ set(HEADER_FILES rendering/screenspaceimagelocal.h rendering/screenspaceimageonline.h rendering/screenspacerenderablerenderable.h + rendering/screenspacetimevaryingimageonline.h rotation/timelinerotation.h rotation/constantrotation.h rotation/fixedrotation.h @@ -151,6 +152,7 @@ set(SOURCE_FILES rendering/screenspaceimagelocal.cpp rendering/screenspaceimageonline.cpp rendering/screenspacerenderablerenderable.cpp + rendering/screenspacetimevaryingimageonline.cpp rotation/timelinerotation.cpp rotation/constantrotation.cpp rotation/fixedrotation.cpp diff --git a/modules/base/basemodule.cpp b/modules/base/basemodule.cpp index d359e51057..ff2c14dbe4 100644 --- a/modules/base/basemodule.cpp +++ b/modules/base/basemodule.cpp @@ -71,6 +71,7 @@ #include #include #include +#include #include #include #include @@ -113,12 +114,13 @@ void BaseModule::internalInitialize(const ghoul::Dictionary&) { ghoul_assert(fSsRenderable, "ScreenSpaceRenderable factory was not created"); fSsRenderable->registerClass("ScreenSpaceDashboard"); + fSsRenderable->registerClass("ScreenSpaceFramebuffer"); fSsRenderable->registerClass("ScreenSpaceImageLocal"); fSsRenderable->registerClass("ScreenSpaceImageOnline"); - fSsRenderable->registerClass("ScreenSpaceFramebuffer"); fSsRenderable->registerClass( "ScreenSpaceRenderableRenderable" ); + fSsRenderable->registerClass("ScreenSpaceTimeVaryingImageOnline"); ghoul::TemplateFactory* fDashboard = @@ -306,6 +308,7 @@ std::vector BaseModule::documentations() const { ScreenSpaceImageLocal::Documentation(), ScreenSpaceImageOnline::Documentation(), ScreenSpaceRenderableRenderable::Documentation(), + ScreenSpaceTimeVaryingImageOnline::Documentation(), ConstantRotation::Documentation(), FixedRotation::Documentation(), diff --git a/modules/base/rendering/screenspacetimevaryingimageonline.cpp b/modules/base/rendering/screenspacetimevaryingimageonline.cpp new file mode 100644 index 0000000000..72c1567479 --- /dev/null +++ b/modules/base/rendering/screenspacetimevaryingimageonline.cpp @@ -0,0 +1,242 @@ +/***************************************************************************************** + * * + * 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 +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "ScreenSpaceTimeVaryingImageOnline"; + + constexpr openspace::properties::Property::PropertyInfo FileInfo = { + "FilePath", + "File Path", + "The file path to the data containing information about when to display which image.", + openspace::properties::Property::Visibility::User + }; + + // This `ScreenSpaceRenderable` displays an image based on the current in-game + // simulation time. The image shown is selected from a JSON file containing + // timestamp-URL pairs. The image with the closest timestamp before or equal to the + // current time is displayed. + // + // Example JSON format: + // { + // "files": [ + // {"timestamp": "2024-05-10 15:00:00.0","url": "https://example.com/image1.png"}, + // {"timestamp": "2024-05-10 15:02:00.0","url": "https://example.com/image2.png"} + // ] + // } + struct [[codegen::Dictionary(ScreenSpaceTimeVaryingImageOnline)]] Parameters { + // [[codegen::verbatim(FileInfo.description)]] + std::filesystem::path filePath; + }; +#include "screenspacetimevaryingimageonline_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation ScreenSpaceTimeVaryingImageOnline::Documentation() { + return codegen::doc("base_screenspace_time_varying_image_online"); +} + +ScreenSpaceTimeVaryingImageOnline::ScreenSpaceTimeVaryingImageOnline( + const ghoul::Dictionary& dictionary) + : ScreenSpaceRenderable(dictionary) + , _jsonFilePath(FileInfo, "") +{ + const Parameters p = codegen::bake(dictionary); + + _jsonFilePath = p.filePath.string(); + _jsonFilePath.onChange([this]() { + loadJsonData(_jsonFilePath.value()); + }); + addProperty(_jsonFilePath); +} + +bool ScreenSpaceTimeVaryingImageOnline::initialize() { + const bool ret = ScreenSpaceRenderable::initialize(); + loadJsonData(_jsonFilePath.value()); + return ret; +} + +bool ScreenSpaceTimeVaryingImageOnline::deinitializeGL() { + _texture = nullptr; + return ScreenSpaceRenderable::deinitializeGL(); +} + +void ScreenSpaceTimeVaryingImageOnline::loadJsonData(const std::filesystem::path& path) { + std::ifstream file = std::ifstream(path); + if (!file.is_open()) { + throw ghoul::RuntimeError(std::format("Could not open JSON file at '{}'", path)); + } + + nlohmann::json json; + file >> json; + + if (json.find("files") == json.end()) { + throw ghoul::RuntimeError(std::format( + "Error loading JSON file. No 'files' was found in '{}'", path + )); + } + + _timestamps.clear(); + _urls.clear(); + + for (const nlohmann::json& entry : json["files"]) { + const std::string& timestamp = entry["timestamp"].get(); + double j2000 = Time::convertTime(timestamp); + _timestamps.push_back(j2000); + _urls[j2000] = entry["url"].get(); + } + + std::sort(_timestamps.begin(), _timestamps.end()); + computeSequenceEndTime(); +} + +void ScreenSpaceTimeVaryingImageOnline::computeSequenceEndTime() { + if (_timestamps.size() <= 1) { + return; + } + + double first = _timestamps.front(); + double last = _timestamps.back(); + double avg = (last - first) / static_cast(_timestamps.size() - 1); + // Extend end time so the last value remains visible for one more interval + _sequenceEndTime = last + avg; +} + +void ScreenSpaceTimeVaryingImageOnline::update() { + if (_timestamps.empty()) { + return; + } + + const double current = global::timeManager->time().j2000Seconds(); + + if (current < _timestamps.front() || current >= _sequenceEndTime) { + _activeIndex = -1; + _texture = nullptr; + _currentUrl.clear(); + return; + } + + if (current >= _timestamps.front() && current < _sequenceEndTime) { + if (int idx = activeIndex(current); idx != _activeIndex) { + _activeIndex = idx; + std::string url = _urls[_timestamps[_activeIndex]]; + if (_currentUrl != url) { + _currentUrl = url; + loadImage(url); + } + } + } + else { + _activeIndex = -1; + } + + if (_imageFuture.valid() && DownloadManager::futureReady(_imageFuture)) { + const DownloadManager::MemoryFile imageFile = _imageFuture.get(); + _imageFuture = std::future(); + if (imageFile.corrupted) { + LERROR(std::format("Error loading image from URL '{}'", _currentUrl)); + return; + } + + try { + std::unique_ptr texture = + ghoul::io::TextureReader::ref().loadTexture( + reinterpret_cast(imageFile.buffer), + imageFile.size, + 2, + imageFile.format + ); + + if (texture) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + if (texture->format() == ghoul::opengl::Texture::Format::Red) { + texture->setSwizzleMask({ GL_RED, GL_RED, GL_RED, GL_ONE }); + } + + texture->uploadTexture(); + texture->setFilter(ghoul::opengl::Texture::FilterMode::LinearMipMap); + texture->purgeFromRAM(); + + _texture = std::move(texture); + _objectSize = _texture->dimensions(); + } + } + catch (const ghoul::io::TextureReader::InvalidLoadException& e) { + LERRORC(e.component, e.message); + } + } +} + +int ScreenSpaceTimeVaryingImageOnline::activeIndex(double currentTime) const { + if (_timestamps.empty()) { + return -1; + } + + auto it = std::upper_bound(_timestamps.begin(), _timestamps.end(), currentTime); + if (it == _timestamps.begin()) { + return 0; + } + else if (it != _timestamps.end()) { + return static_cast(std::distance(_timestamps.begin(), it)) - 1; + } + else { + return static_cast(_timestamps.size()) - 1; + } +} + +void ScreenSpaceTimeVaryingImageOnline::loadImage(const std::string& imageUrl) { + if (_imageFuture.valid()) { + return; + } + + _imageFuture = global::downloadManager->fetchFile( + imageUrl, + [](const DownloadManager::MemoryFile&) {}, + [](const std::string& e) { + LERROR(std::format("Download failed: {}", e)); + } + ); +} + +void ScreenSpaceTimeVaryingImageOnline::bindTexture() { + if (_texture) [[likely]] { + _texture->bind(); + } +} + +} // namespace openspace diff --git a/modules/base/rendering/screenspacetimevaryingimageonline.h b/modules/base/rendering/screenspacetimevaryingimageonline.h new file mode 100644 index 0000000000..ea8773ba13 --- /dev/null +++ b/modules/base/rendering/screenspacetimevaryingimageonline.h @@ -0,0 +1,71 @@ +/***************************************************************************************** + * * + * 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___SCREENSPACETIMEVARYINGIMAGEONLINE___H__ +#define __OPENSPACE_MODULE_BASE___SCREENSPACETIMEVARYINGIMAGEONLINE___H__ + +#include + +#include +#include +#include + +namespace ghoul::opengl { class Texture; } + +namespace openspace { + +namespace documentation { struct Documentation; } + +class ScreenSpaceTimeVaryingImageOnline : public ScreenSpaceRenderable { +public: + explicit ScreenSpaceTimeVaryingImageOnline(const ghoul::Dictionary& dictionary); + + bool initialize() override; + bool deinitializeGL() override; + void update() override; + + static documentation::Documentation Documentation(); + +private: + void bindTexture() override; + void loadJsonData(const std::filesystem::path& path); + void computeSequenceEndTime(); + void loadImage(const std::string& imageUrl); + int activeIndex(double currentTime) const; + + properties::StringProperty _jsonFilePath; + + std::future _imageFuture; + std::map _urls; + std::string _currentUrl; + std::unique_ptr _texture; + std::vector _timestamps; + + int _activeIndex = -1; + double _sequenceEndTime = 0.0; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___SCREENSPACETIMEVARYINGIMAGEONLINE___H__