/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2023 * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _MSC_VER #pragma warning (push) // CPL throws warning about missing DLL interface #pragma warning (disable : 4251) #endif // _MSC_VER #include #ifdef _MSC_VER #pragma warning (pop) #endif // _MSC_VER #include "globebrowsingmodule_lua.inl" namespace { constexpr std::string_view _loggerCat = "GlobeBrowsingModule"; constexpr openspace::properties::Property::PropertyInfo TileCacheSizeInfo = { "TileCacheSize", "Tile Cache Size", "The maximum size of the MemoryAwareTileCache, on the CPU and GPU", openspace::properties::Property::Visibility::AdvancedUser }; constexpr openspace::properties::Property::PropertyInfo DefaultGeoPointTextureInfo = { "DefaultGeoPointTexture", "Default Geo Point Texture", "A path to a texture to use as default for GeoJson points" }; constexpr openspace::properties::Property::PropertyInfo MRFCacheEnabledInfo = { "MRFCacheEnabled", "MRF Cache Enabled", "Determines whether automatic caching of globe browsing data is enabled.", openspace::properties::Property::Visibility::AdvancedUser }; constexpr openspace::properties::Property::PropertyInfo MRFCacheLocationInfo = { "MRFCacheLocation", "MRF Cache Location", "The location of the root folder for the MRF cache of globe browsing data.", openspace::properties::Property::Visibility::AdvancedUser }; openspace::GlobeBrowsingModule::Capabilities parseSubDatasets(char** subDatasets, int nSubdatasets) { // Idea: Iterate over the list of sublayers keeping a current layer and identify // it by its number. If this number changes, we know that we have a new // layer using Layer = openspace::GlobeBrowsingModule::Layer; std::vector result; int currentLayerNumber = -1; Layer currentLayer; for (int i = 0; i < nSubdatasets; ++i) { int iDataset = -1; std::array IdentifierBuffer; std::fill(IdentifierBuffer.begin(), IdentifierBuffer.end(), '\0'); int ret = sscanf( subDatasets[i], "SUBDATASET_%i_%255[^=]", &iDataset, IdentifierBuffer.data() ); if (ret != 2) { LERROR("Error parsing dataset"); continue; } if (iDataset != currentLayerNumber) { // We are done with the previous version result.push_back(std::move(currentLayer)); currentLayer = Layer(); currentLayerNumber = iDataset; } const std::string identifier = std::string(IdentifierBuffer.data()); const std::string ds(subDatasets[i]); const std::string value = ds.substr(ds.find_first_of('=') + 1); // The DESC/NAME difference is not a typo if (identifier == "DESC") { currentLayer.name = value; } else if (identifier == "NAME") { currentLayer.url = value; } else { LINFOC( "GlobeBrowsingGUI", "Unknown subdataset identifier: " + identifier ); } } return result; } struct [[codegen::Dictionary(GlobeBrowsingModule)]] Parameters { // [[codegen::verbatim(TileCacheSizeInfo.description)]] std::optional tileCacheSize; // [[codegen::verbatim(DefaultGeoPointTextureInfo.description)]] std::optional defaultGeoPointTexture; // [[codegen::verbatim(MRFCacheEnabledInfo.description)]] std::optional mrfCacheEnabled [[codegen::key("MRFCacheEnabled")]]; // [[codegen::verbatim(MRFCacheLocationInfo.description)]] std::optional mrfCacheLocation [[codegen::key("MRFCacheLocation")]]; }; #include "globebrowsingmodule_codegen.cpp" } // namespace namespace openspace { GlobeBrowsingModule::GlobeBrowsingModule() : OpenSpaceModule(Name) , _tileCacheSizeMB(TileCacheSizeInfo, 1024) , _defaultGeoPointTexturePath(DefaultGeoPointTextureInfo) , _mrfCacheEnabled(MRFCacheEnabledInfo, false) , _mrfCacheLocation(MRFCacheLocationInfo, "${BASE}/cache_mrf") { addProperty(_tileCacheSizeMB); addProperty(_defaultGeoPointTexturePath); addProperty(_mrfCacheEnabled); addProperty(_mrfCacheLocation); } void GlobeBrowsingModule::internalInitialize(const ghoul::Dictionary& dict) { using namespace globebrowsing; const Parameters p = codegen::bake(dict); _tileCacheSizeMB = p.tileCacheSize.value_or(_tileCacheSizeMB); _defaultGeoPointTexturePath.onChange([this]() { if (_defaultGeoPointTexturePath.value().empty()) { _hasDefaultGeoPointTexture = false; return; } std::filesystem::path path = _defaultGeoPointTexturePath.value(); if (std::filesystem::exists(path)) { _hasDefaultGeoPointTexture = true; } else { LWARNINGC("GlobeBrowsingModule", fmt::format( "The provided texture file {} for the default geo point texture " "does not exist", path )); } }); if (p.defaultGeoPointTexture.has_value()) { _defaultGeoPointTexturePath = absPath(*p.defaultGeoPointTexture).string(); } _mrfCacheEnabled = p.mrfCacheEnabled.value_or(_mrfCacheEnabled); _mrfCacheLocation = p.mrfCacheLocation.value_or(_mrfCacheLocation); // Initialize global::callback::initializeGL->emplace_back([this]() { ZoneScopedN("GlobeBrowsingModule"); _tileCache = std::make_unique(_tileCacheSizeMB); addPropertySubOwner(_tileCache.get()); TileProvider::initializeDefaultTile(); // Convert from MB to Bytes GdalWrapper::create( 512ULL * 1024ULL * 1024ULL, // 512 MB static_cast(CpuCap.installedMainMemory() * 0.25 * 1024 * 1024) ); addPropertySubOwner(GdalWrapper::ref()); }); global::callback::deinitializeGL->emplace_back([]() { ZoneScopedN("GlobeBrowsingModule"); TileProvider::deinitializeDefaultTile(); }); // Render global::callback::render->emplace_back([this]() { ZoneScopedN("GlobeBrowsingModule"); _tileCache->update(); }); // Deinitialize global::callback::deinitialize->emplace_back([]() { ZoneScopedN("GlobeBrowsingModule"); GdalWrapper::destroy(); }); ghoul::TemplateFactory* fRenderable = FactoryManager::ref().factory(); ghoul_assert(fRenderable, "Renderable factory was not created"); fRenderable->registerClass("RenderableGlobe"); ghoul::TemplateFactory* fTranslation = FactoryManager::ref().factory(); ghoul_assert(fTranslation, "Translation factory was not created"); fTranslation->registerClass("GlobeTranslation"); ghoul::TemplateFactory* fRotation = FactoryManager::ref().factory(); ghoul_assert(fRotation, "Rotation factory was not created"); fRotation->registerClass("GlobeRotation"); FactoryManager::ref().addFactory("TileProvider"); ghoul::TemplateFactory* fTileProvider = FactoryManager::ref().factory(); ghoul_assert(fTileProvider, "TileProvider factory was not created"); fTileProvider->registerClass("DefaultTileLayer"); fTileProvider->registerClass("SingleImageTileLayer"); fTileProvider->registerClass("ImageSequenceTileLayer"); fTileProvider->registerClass("SpoutImageTileLayer"); fTileProvider->registerClass("TemporalTileLayer"); fTileProvider->registerClass("TileIndexTileLayer"); fTileProvider->registerClass("SizeReferenceTileLayer"); fTileProvider->registerClass("ByLevelTileLayer"); fTileProvider->registerClass("ByIndexTileLayer"); ghoul::TemplateFactory* fDashboard = FactoryManager::ref().factory(); ghoul_assert(fDashboard, "Dashboard factory was not created"); fDashboard->registerClass("DashboardItemGlobeLocation"); } globebrowsing::cache::MemoryAwareTileCache* GlobeBrowsingModule::tileCache() { return _tileCache.get(); } std::vector GlobeBrowsingModule::documentations() const { return { globebrowsing::Layer::Documentation(), globebrowsing::LayerAdjustment::Documentation(), globebrowsing::LayerManager::Documentation(), globebrowsing::GlobeTranslation::Documentation(), globebrowsing::GlobeRotation::Documentation(), globebrowsing::RenderableGlobe::Documentation(), globebrowsing::DefaultTileProvider::Documentation(), globebrowsing::ImageSequenceTileProvider::Documentation(), globebrowsing::SingleImageProvider::Documentation(), globebrowsing::SizeReferenceTileProvider::Documentation(), globebrowsing::TemporalTileProvider::Documentation(), globebrowsing::TileProviderByIndex::Documentation(), globebrowsing::TileProviderByLevel::Documentation(), globebrowsing::GeoJsonManager::Documentation(), globebrowsing::GeoJsonComponent::Documentation(), globebrowsing::GeoJsonProperties::Documentation(), GlobeLabelsComponent::Documentation(), RingsComponent::Documentation(), ShadowComponent::Documentation() }; } void GlobeBrowsingModule::goToChunk(const globebrowsing::RenderableGlobe& globe, int x, int y, int level) { ghoul_assert(level < std::numeric_limits::max(), "Level way too big"); goToChunk( globe, globebrowsing::TileIndex(x, y, static_cast(level)), glm::vec2(0.5f, 0.5f), true ); } void GlobeBrowsingModule::goToGeo(const globebrowsing::RenderableGlobe& globe, double latitude, double longitude) { using namespace globebrowsing; goToGeodetic2( globe, Geodetic2{ glm::radians(latitude), glm::radians(longitude) }, true ); } void GlobeBrowsingModule::goToGeo(const globebrowsing::RenderableGlobe& globe, double latitude, double longitude, double altitude) { using namespace globebrowsing; goToGeodetic3( globe, { Geodetic2{ glm::radians(latitude), glm::radians(longitude) }, altitude }, true ); } glm::vec3 GlobeBrowsingModule::cartesianCoordinatesFromGeo( const globebrowsing::RenderableGlobe& globe, double latitude, double longitude, double altitude) { using namespace globebrowsing; const Geodetic3 pos = { { .lat = glm::radians(latitude), .lon = glm::radians(longitude) }, altitude }; return glm::vec3(globe.ellipsoid().cartesianPosition(pos)); } glm::dvec3 GlobeBrowsingModule::geoPosition() const { using namespace globebrowsing; const SceneGraphNode* n = global::navigationHandler->orbitalNavigator().anchorNode(); if (!n) { return glm::dvec3(0.0); } const RenderableGlobe* globe = dynamic_cast(n->renderable()); if (!globe) { return glm::dvec3(0.0); } const glm::dvec3 cameraPosition = global::navigationHandler->camera()->positionVec3(); const glm::dmat4 inverseModelTransform = glm::inverse(n->modelTransform()); const glm::dvec3 cameraPositionModelSpace = glm::dvec3(inverseModelTransform * glm::dvec4(cameraPosition, 1.0)); const SurfacePositionHandle posHandle = globe->calculateSurfacePositionHandle( cameraPositionModelSpace ); const Geodetic2 geo2 = globe->ellipsoid().cartesianToGeodetic2( posHandle.centerToReferenceSurface ); double lat = glm::degrees(geo2.lat); double lon = glm::degrees(geo2.lon); double altitude = glm::length( cameraPositionModelSpace - posHandle.centerToReferenceSurface ); if (glm::length(cameraPositionModelSpace) < glm::length(posHandle.centerToReferenceSurface)) { altitude = -altitude; } return glm::dvec3(lat, lon, altitude); } void GlobeBrowsingModule::goToChunk(const globebrowsing::RenderableGlobe& globe, const globebrowsing::TileIndex& ti, glm::vec2 uv, bool doResetCameraDirection) { using namespace globebrowsing; const GeodeticPatch patch(ti); const Geodetic2 corner = patch.corner(SOUTH_WEST); Geodetic2 positionOnPatch = patch.size(); positionOnPatch.lat *= uv.y; positionOnPatch.lon *= uv.x; const Geodetic2 pointPosition = { .lat = corner.lat + positionOnPatch.lat, .lon = corner.lon + positionOnPatch.lon }; // Compute altitude const glm::dvec3 cameraPosition = global::navigationHandler->camera()->positionVec3(); SceneGraphNode* globeSceneGraphNode = dynamic_cast(globe.owner()); if (!globeSceneGraphNode) { LERROR("Cannot go to chunk. The renderable is not attached to scene graph node"); return; } const glm::dmat4 inverseModelTransform = glm::inverse( globeSceneGraphNode->modelTransform() ); const glm::dvec3 cameraPositionModelSpace = glm::dvec3(inverseModelTransform * glm::dvec4(cameraPosition, 1.0)); const SurfacePositionHandle posHandle = globe.calculateSurfacePositionHandle( cameraPositionModelSpace ); const double altitude = glm::length( cameraPositionModelSpace - posHandle.centerToReferenceSurface ); goToGeodetic3(globe, { pointPosition, altitude }, doResetCameraDirection); } void GlobeBrowsingModule::goToGeodetic2(const globebrowsing::RenderableGlobe& globe, globebrowsing::Geodetic2 geo2, bool doResetCameraDirection) { using namespace globebrowsing; const glm::dvec3 cameraPosition = global::navigationHandler->camera()->positionVec3(); SceneGraphNode* globeSceneGraphNode = dynamic_cast(globe.owner()); if (!globeSceneGraphNode) { LERROR("Error when going to Geodetic2"); } const glm::dmat4 inverseModelTransform = glm::inverse( globeSceneGraphNode->modelTransform() ); const glm::dvec3 cameraPositionModelSpace = glm::dvec3(inverseModelTransform * glm::dvec4(cameraPosition, 1.0)); const SurfacePositionHandle posHandle = globe.calculateSurfacePositionHandle( cameraPositionModelSpace ); const glm::dvec3 centerToActualSurface = posHandle.centerToReferenceSurface + posHandle.referenceSurfaceOutDirection * posHandle.heightToSurface; const double altitude = glm::length(cameraPositionModelSpace - centerToActualSurface); goToGeodetic3(globe, { geo2, altitude }, doResetCameraDirection); } void GlobeBrowsingModule::goToGeodetic3(const globebrowsing::RenderableGlobe& globe, globebrowsing::Geodetic3 geo3, bool) { using namespace globebrowsing; const glm::dvec3 positionModelSpace = globe.ellipsoid().cartesianPosition(geo3); const glm::dvec3 slightlyNorth = globe.ellipsoid().cartesianSurfacePosition( Geodetic2{ geo3.geodetic2.lat + 0.001, geo3.geodetic2.lon } ); interaction::NavigationState state; state.anchor = globe.owner()->identifier(); state.referenceFrame = globe.owner()->identifier(); state.position = positionModelSpace; state.up = slightlyNorth; global::navigationHandler->setNavigationStateNextFrame(state); } glm::dquat GlobeBrowsingModule::lookDownCameraRotation( const globebrowsing::RenderableGlobe& globe, glm::dvec3 cameraModelSpace, globebrowsing::Geodetic2 geo2) { using namespace globebrowsing; // Lookup vector const glm::dvec3 positionModelSpace = globe.ellipsoid().cartesianSurfacePosition( geo2 ); const glm::dvec3 slightlyNorth = globe.ellipsoid().cartesianSurfacePosition( Geodetic2{ geo2.lat + 0.001, geo2.lon } ); const glm::dvec3 lookUpModelSpace = glm::normalize( slightlyNorth - positionModelSpace ); // Matrix const glm::dmat4 lookAtMatrix = glm::lookAt(cameraModelSpace, positionModelSpace, lookUpModelSpace); // Set rotation const glm::dquat rotation = glm::quat_cast(inverse(lookAtMatrix)); return rotation; } const globebrowsing::RenderableGlobe* GlobeBrowsingModule::castFocusNodeRenderableToGlobe() { using namespace globebrowsing; const Renderable* renderable = global::navigationHandler->orbitalNavigator().anchorNode()->renderable(); if (!renderable) { return nullptr; } using namespace globebrowsing; if (const RenderableGlobe* globe = dynamic_cast(renderable)) { return globe; } else { return nullptr; } } void GlobeBrowsingModule::loadWMSCapabilities(std::string name, std::string globe, std::string url) { auto downloadFunction = [](const std::string& downloadUrl) { LDEBUG("Opening WMS capabilities: " + downloadUrl); GDALDatasetH dataset = GDALOpen( downloadUrl.c_str(), GA_ReadOnly ); if (!dataset) { LWARNING("Could not open dataset: " + downloadUrl); return Capabilities(); } char** subDatasets = GDALGetMetadata(dataset, "SUBDATASETS"); const int nSubdatasets = CSLCount(subDatasets); Capabilities cap = parseSubDatasets(subDatasets, nSubdatasets); GDALClose(dataset); LDEBUG("Finished WMS capabilities: " + downloadUrl); return cap; }; _inFlightCapabilitiesMap[name] = std::async( std::launch::async, downloadFunction, url ); //_capabilitiesMap[name] = downloadFunction(url); _urlList.emplace(std::move(globe), UrlInfo{ std::move(name), std::move(url) }); } GlobeBrowsingModule::Capabilities GlobeBrowsingModule::capabilities(const std::string& name) { // First check the ones that have already finished const auto it = _capabilitiesMap.find(name); if (it != _capabilitiesMap.end()) { return it->second; } else { const auto inFlightIt = _inFlightCapabilitiesMap.find(name); if (inFlightIt != _inFlightCapabilitiesMap.end()) { // If the download and the parsing has not finished yet, this will block, // otherwise it will just return const Capabilities cap = inFlightIt->second.get(); _capabilitiesMap[name] = cap; _inFlightCapabilitiesMap.erase(inFlightIt); return cap; } else { return {}; } } } void GlobeBrowsingModule::removeWMSServer(const std::string& name) { // First delete all the capabilities that are currently in flight const auto inFlightIt = _inFlightCapabilitiesMap.find(name); if (inFlightIt != _inFlightCapabilitiesMap.end()) { _inFlightCapabilitiesMap.erase(inFlightIt); } // Then download the ones that are already finished const auto capIt = _capabilitiesMap.find(name); if (capIt != _capabilitiesMap.end()) { _capabilitiesMap.erase(capIt); } // Then remove the calues from the globe server list for (auto it = _urlList.begin(); it != _urlList.end();) { // We have to increment first because the erase will invalidate the iterator const auto eraseIt = it++; if (eraseIt->second.name == name) { _urlList.erase(eraseIt); } } } std::vector GlobeBrowsingModule::urlInfo(const std::string& globe) const { const auto range = _urlList.equal_range(globe); std::vector res; for (auto i = range.first; i != range.second; ++i) { res.emplace_back(i->second); } return res; } bool GlobeBrowsingModule::hasUrlInfo(const std::string& globe) const { return _urlList.find(globe) != _urlList.end(); } bool GlobeBrowsingModule::isMRFCachingEnabled() const { return _mrfCacheEnabled; } const std::string GlobeBrowsingModule::mrfCacheLocation() const { return _mrfCacheLocation; } bool GlobeBrowsingModule::hasDefaultGeoPointTexture() const { return _hasDefaultGeoPointTexture; } std::string_view GlobeBrowsingModule::defaultGeoPointTexture() const { return _defaultGeoPointTexturePath; } scripting::LuaLibrary GlobeBrowsingModule::luaLibrary() const { return { .name = "globebrowsing", .functions = { codegen::lua::AddLayer, codegen::lua::DeleteLayer, codegen::lua::GetLayers, codegen::lua::MoveLayer, codegen::lua::GoToChunk, codegen::lua::GoToGeo, // @TODO (2021-06-23, emmbr) Combine with the above function when the camera // paths work really well close to surfaces codegen::lua::FlyToGeo, codegen::lua::GetLocalPositionFromGeo, codegen::lua::GetGeoPositionForCamera, codegen::lua::LoadWMSCapabilities, codegen::lua::RemoveWMSServer, codegen::lua::CapabilitiesWMS, codegen::lua::AddGeoJson, codegen::lua::DeleteGeoJson, codegen::lua::AddGeoJsonFromFile, }, .scripts = { absPath("${MODULE_GLOBEBROWSING}/scripts/layer_support.lua"), absPath("${MODULE_GLOBEBROWSING}/scripts/node_support.lua") } }; } } // namespace openspace