From c714a7f57dbc8eb5ced4d8116e81341b5441c058 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Sat, 15 Apr 2023 11:35:28 +0200 Subject: [PATCH] Feature/geojson (#2595) Add the option to add geojson components to globes, from geojson files. One geojson file creates one GeoJsonComponent, which in turn may contain multiple GlobeGeometryFeatures Geojson is a format that supports points, lines, and polygons. In addition to the basic functionality, extra features have been added that will long-term allow rendering the geometry needed to represent KML files (another format for geospatial geometry data). Here are links to references for both formats: Geojson: https://geojson.org/ KML: https://developers.google.com/kml/documentation/kmlreference data/assets/examples/geojson includes some example files that I have used for testing. Any geojson file can also be added through drag-n-drop. Note however that you might need to change the AltitudeMode or HeightOffset properties for the feature to be visible. --- .gitmodules | 3 + .../geojson_extruded_shaded_polygon.asset | 49 + .../examples/geojson/geojson_lines.asset | 39 + .../geojson/geojson_multiple_polygons.asset | 58 ++ .../examples/geojson/geojson_points.asset | 40 + .../geojson/geojson_points_outfacing.asset | 41 + .../geojson/geojson_polygons_with_holes.asset | 39 + .../examples/geojson/geojson_roverpath.asset | 41 + .../geojson_toronto_neighborhoods.asset | 42 + data/globe_pin.png | Bin 0 -> 11909 bytes include/openspace/rendering/fadeable.h | 2 +- include/openspace/rendering/helper.h | 24 + modules/globebrowsing/CMakeLists.txt | 20 + modules/globebrowsing/ext/geos | 1 + modules/globebrowsing/globebrowsingmodule.cpp | 56 +- modules/globebrowsing/globebrowsingmodule.h | 6 + .../globebrowsing/globebrowsingmodule_lua.inl | 116 +++ modules/globebrowsing/shaders/geojson_fs.glsl | 76 ++ .../shaders/geojson_points_fs.glsl | 62 ++ .../shaders/geojson_points_gs.glsl | 158 +++ .../shaders/geojson_points_vs.glsl | 38 + modules/globebrowsing/shaders/geojson_vs.glsl | 64 ++ .../src/geojson/geojsoncomponent.cpp | 797 +++++++++++++++ .../src/geojson/geojsoncomponent.h | 163 ++++ .../src/geojson/geojsonmanager.cpp | 146 +++ .../src/geojson/geojsonmanager.h | 74 ++ .../src/geojson/geojsonproperties.cpp | 617 ++++++++++++ .../src/geojson/geojsonproperties.h | 151 +++ .../src/geojson/globegeometryfeature.cpp | 914 ++++++++++++++++++ .../src/geojson/globegeometryfeature.h | 220 +++++ .../src/geojson/globegeometryhelper.cpp | 360 +++++++ .../src/geojson/globegeometryhelper.h | 109 +++ modules/globebrowsing/src/renderableglobe.cpp | 19 + modules/globebrowsing/src/renderableglobe.h | 6 + modules/imgui/ext/imgui/imgui_demo.cpp | 2 +- openspace.cfg | 3 +- scripts/drag_drop_handler.lua | 2 + src/rendering/helper.cpp | 23 + src/rendering/texturecomponent.cpp | 9 +- 39 files changed, 4583 insertions(+), 7 deletions(-) create mode 100644 data/assets/examples/geojson/geojson_extruded_shaded_polygon.asset create mode 100644 data/assets/examples/geojson/geojson_lines.asset create mode 100644 data/assets/examples/geojson/geojson_multiple_polygons.asset create mode 100644 data/assets/examples/geojson/geojson_points.asset create mode 100644 data/assets/examples/geojson/geojson_points_outfacing.asset create mode 100644 data/assets/examples/geojson/geojson_polygons_with_holes.asset create mode 100644 data/assets/examples/geojson/geojson_roverpath.asset create mode 100644 data/assets/examples/geojson/geojson_toronto_neighborhoods.asset create mode 100644 data/globe_pin.png create mode 160000 modules/globebrowsing/ext/geos create mode 100644 modules/globebrowsing/shaders/geojson_fs.glsl create mode 100644 modules/globebrowsing/shaders/geojson_points_fs.glsl create mode 100644 modules/globebrowsing/shaders/geojson_points_gs.glsl create mode 100644 modules/globebrowsing/shaders/geojson_points_vs.glsl create mode 100644 modules/globebrowsing/shaders/geojson_vs.glsl create mode 100644 modules/globebrowsing/src/geojson/geojsoncomponent.cpp create mode 100644 modules/globebrowsing/src/geojson/geojsoncomponent.h create mode 100644 modules/globebrowsing/src/geojson/geojsonmanager.cpp create mode 100644 modules/globebrowsing/src/geojson/geojsonmanager.h create mode 100644 modules/globebrowsing/src/geojson/geojsonproperties.cpp create mode 100644 modules/globebrowsing/src/geojson/geojsonproperties.h create mode 100644 modules/globebrowsing/src/geojson/globegeometryfeature.cpp create mode 100644 modules/globebrowsing/src/geojson/globegeometryfeature.h create mode 100644 modules/globebrowsing/src/geojson/globegeometryhelper.cpp create mode 100644 modules/globebrowsing/src/geojson/globegeometryhelper.h diff --git a/.gitmodules b/.gitmodules index e599dbca0e..76c093dc48 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,6 +32,9 @@ [submodule "support/coding/codegen"] path = support/coding/codegen url = https://github.com/OpenSpace/codegen +[submodule "modules/globebrowsing/ext/geos"] + path = modules/globebrowsing/ext/geos + url = https://github.com/OpenSpace/geos.git [submodule "documentation"] path = documentation url = https://github.com/OpenSpace/OpenSpace-Documentation-Dist.git diff --git a/data/assets/examples/geojson/geojson_extruded_shaded_polygon.asset b/data/assets/examples/geojson/geojson_extruded_shaded_polygon.asset new file mode 100644 index 0000000000..b3400ec522 --- /dev/null +++ b/data/assets/examples/geojson/geojson_extruded_shaded_polygon.asset @@ -0,0 +1,49 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local sun = asset.require("scene/solarsystem/sun/sun") + +local data = asset.syncedResource({ + Name = "GeoJSON Example Africa", + Type = "UrlSynchronization", + Identifier = "geojson_example_polygon_extruded_africa", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/polygon_extruded_africa.geojson" +}) + +local Example_Polygon = { + Identifier = "Earth-Polygon-withLights", + File = data .. "polygon_extruded_africa.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + Color = { 0.0, 1.0, 0.0 }, + FillColor = { 0.5, 0.6, 0.5 }, + LineWidth = 0.5, + FillOpacity = 1.0, + PerformShading = true + }, + LightSources = { + sun.LightSource + }, + Name = "Extruded and Shaded Polygon (lit by Sun)" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Polygon) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Polygon) +end) + +asset.export(Example_Polygon) + + +asset.meta = { + Name = "GeoJson Example - Extruded and Shaded Polygon", + Version = "1.0", + Description = [[GeoJson example asset demonstrating how to apply shading from light + sources on polygons]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_lines.asset b/data/assets/examples/geojson/geojson_lines.asset new file mode 100644 index 0000000000..07ce734622 --- /dev/null +++ b/data/assets/examples/geojson/geojson_lines.asset @@ -0,0 +1,39 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Lines", + Type = "UrlSynchronization", + Identifier = "geojson_example_lines", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/lines.geojson" +}) + +local Example_Lines = { + Identifier = "Lines-Example", + File = data .. "lines.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + LineWidth = 2.0 + }, + Name = "Example Lines" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Lines) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Lines) +end) + +asset.export(Example_Lines) + + +asset.meta = { + Name = "GeoJson Example - Lines", + Version = "1.0", + Description = [[GeoJson example asset with lines]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_multiple_polygons.asset b/data/assets/examples/geojson/geojson_multiple_polygons.asset new file mode 100644 index 0000000000..a61d676a30 --- /dev/null +++ b/data/assets/examples/geojson/geojson_multiple_polygons.asset @@ -0,0 +1,58 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Polygon Multiple", + Type = "UrlSynchronization", + Identifier = "geojson_example_polygon_multiple", + Url = { + "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/polygon_multiple.geojson", + "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/polygon_different_heights.geojson" + } +}) + +local Example_Polygon = { + Identifier = "Earth-Polygon", + File = data .. "polygon_multiple.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + Color = { 1.0, 0.0, 0.0 }, + LineWidth = 2.0 + }, + Name = "Polygon (Multiple)" +} + +local Example_Polygon_Diff_Heights = { + Identifier = "Earth-Polygon-Different-Heights", + File = data .. "polygon_different_heights.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + Color = { 0.5, 0.0, 1.0 }, + LineWidth = 2.0 + }, + Name = "Polygon (Different heights)", + Description = "A GeoJSON test layer with some different heights" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Polygon) + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Polygon_Diff_Heights) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Polygon) + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Polygon_Diff_Heights) +end) + +asset.export(Example_Polygon) +asset.export(Example_Polygon_Diff_Heights) + + +asset.meta = { + Name = "GeoJson Example - Multiple Polygons", + Version = "1.0", + Description = [[GeoJson example asset with multiple polygons]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_points.asset b/data/assets/examples/geojson/geojson_points.asset new file mode 100644 index 0000000000..f152ebe077 --- /dev/null +++ b/data/assets/examples/geojson/geojson_points.asset @@ -0,0 +1,40 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Outfacing", + Type = "UrlSynchronization", + Identifier = "geojson_example_points", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/points.geojson" +}) + +local Example_Points = { + Identifier = "Points-Example", + File = data .. "points.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + PointSize = 10.0 + }, + Name = "Example Points" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Points) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Points) +end) + +asset.export(Example_Points) + + +asset.meta = { + Name = "GeoJson Example - Points", + Version = "1.0", + Description = [[GeoJson example asset with points that are facing the camera + (default)]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_points_outfacing.asset b/data/assets/examples/geojson/geojson_points_outfacing.asset new file mode 100644 index 0000000000..765d4cefb7 --- /dev/null +++ b/data/assets/examples/geojson/geojson_points_outfacing.asset @@ -0,0 +1,41 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Outfacing", + Type = "UrlSynchronization", + Identifier = "geojson_example_points", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/points.geojson" +}) + +local Example_Points = { + Identifier = "Points-Example-outfacing", + File = data .. "points.geojson", + HeightOffset = 20000.0, + DefaultProperties = { + PointSize = 10.0 + }, + PointRenderMode = "Globe Normal", + Name = "Example Points (align to globe normal)" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Points) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Points) +end) + +asset.export(Example_Points) + + +asset.meta = { + Name = "GeoJson Example - Outfacing Points", + Version = "1.0", + Description = [[GeoJson example asset with point that are aligned to "stick out" of + the globe, i.e. face out of the planet]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_polygons_with_holes.asset b/data/assets/examples/geojson/geojson_polygons_with_holes.asset new file mode 100644 index 0000000000..48747fa337 --- /dev/null +++ b/data/assets/examples/geojson/geojson_polygons_with_holes.asset @@ -0,0 +1,39 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Polygon with holes", + Type = "UrlSynchronization", + Identifier = "geojson_example_polygon_with_holes", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/polygon_with_holes.geojson" +}) + +local Example_Holes = { + Identifier = "PolygonWithHoles", + File = data .. "polygon_with_holes.geojson", + HeightOffset = 2000.0, + DefaultProperties = { + Color = { 0.0, 1.0, 1.0 } + }, + Name = "Example Polygon (holes)" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Holes) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Holes) +end) + +asset.export(Example_Holes) + + +asset.meta = { + Name = "GeoJson Example - Polygon with holes", + Version = "1.0", + Description = [[GeoJson example asset with polygon that has holes in it]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_roverpath.asset b/data/assets/examples/geojson/geojson_roverpath.asset new file mode 100644 index 0000000000..2b699eeed2 --- /dev/null +++ b/data/assets/examples/geojson/geojson_roverpath.asset @@ -0,0 +1,41 @@ +local mars = asset.require('scene/solarsystem/planets/mars/mars') +local marsIdentifier = mars.Mars.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Path Perseverance", + Type = "UrlSynchronization", + Identifier = "geojson_example_path_perseverance", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/path_perseverance.geojson" +}) + +local Example_RoverPath = { + Identifier = "Mars-Perseverance-Path", + File = data .. "path_perseverance.geojson", + HeightOffset = 10.0, + IgnoreHeights = true, -- Ignores height values from the file itself + DefaultProperties = { + LineWidth = 2.0 + }, + Name = "Perseverance Rover Path" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(marsIdentifier, Example_RoverPath) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(marsIdentifier, Example_RoverPath) +end) + +asset.export(Example_RoverPath) + + +asset.meta = { + Name = "GeoJson Example - Rover path", + Version = "1.0", + Description = [[GeoJson example asset that renderes a snapshot of the path of the + Perseverance Rover on Mars. Data from: https://mars.nasa.gov/mars2020/mission/where-is-the-rover/]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/geojson/geojson_toronto_neighborhoods.asset b/data/assets/examples/geojson/geojson_toronto_neighborhoods.asset new file mode 100644 index 0000000000..a7122242ba --- /dev/null +++ b/data/assets/examples/geojson/geojson_toronto_neighborhoods.asset @@ -0,0 +1,42 @@ +local earth = asset.require('scene/solarsystem/planets/earth/earth') +local earthIdentifier = earth.Earth.Identifier + +local data = asset.syncedResource({ + Name = "GeoJSON Example Toronto Neighborhoods", + Type = "UrlSynchronization", + Identifier = "geojson_example_toronto_neighborhoods", + Url = "http://liu-se.cdn.openspaceproject.com/files/examples/geojson/toronto_neighborhoods.geojson" +}) + +local Example_Toronto = { + Identifier = "Toronto-Neighborhoods", + File = data .. "toronto_neighborhoods.geojson", + HeightOffset = 1000.0, + DefaultProperties = { + Color = { 0.0, 1.0, 0.0 }, + FillColor = { 0.2, 0.33, 0.2 }, + LineWidth = 2.0, + }, + Name = "Toronto Neighbourhoods" +} + +asset.onInitialize(function() + openspace.globebrowsing.addGeoJson(earthIdentifier, Example_Toronto) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteGeoJson(earthIdentifier, Example_Toronto) +end) + +asset.export(Example_Toronto) + + +asset.meta = { + Name = "GeoJson Example - Toronto neighborhoods", + Version = "1.0", + Description = [[GeoJson example asset that shows the neighborhoods of the city Toronto, + Canada, as polygons. Data source: https://handsondataviz.org/geojsonio.html]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/globe_pin.png b/data/globe_pin.png new file mode 100644 index 0000000000000000000000000000000000000000..3d530d97448e50ad154df3dcb760937569e2325e GIT binary patch literal 11909 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 convert(std::vector v); @@ -119,6 +129,20 @@ std::vector createRing(int nSegments, float radius, std::pair, std::vector> createSphere(int nSegments, glm::vec3 radii, glm::vec4 colors = glm::vec4(1.f)); +/** + * Data structure that can be used for rendering using multiple light directions + */ +struct LightSourceRenderData { + unsigned int nLightSources = 0; + + // Buffers for uniform uploading + std::vector intensitiesBuffer; + std::vector directionsViewSpaceBuffer; + + void updateBasedOnLightSources(const RenderData& renderData, + const std::vector>& sources); +}; + } // namespace openspace::rendering::helper #endif // __OPENSPACE_CORE___HELPER___H__ diff --git a/modules/globebrowsing/CMakeLists.txt b/modules/globebrowsing/CMakeLists.txt index c299869ea1..ada26b0819 100644 --- a/modules/globebrowsing/CMakeLists.txt +++ b/modules/globebrowsing/CMakeLists.txt @@ -60,6 +60,11 @@ set(HEADER_FILES src/tiletextureinitdata.h src/tilecacheproperties.h src/timequantizer.h + src/geojson/geojsoncomponent.h + src/geojson/geojsonmanager.h + src/geojson/geojsonproperties.h + src/geojson/globegeometryfeature.h + src/geojson/globegeometryhelper.h src/tileprovider/defaulttileprovider.h src/tileprovider/imagesequencetileprovider.h src/tileprovider/singleimagetileprovider.h @@ -101,6 +106,11 @@ set(SOURCE_FILES src/tileloadjob.cpp src/tiletextureinitdata.cpp src/timequantizer.cpp + src/geojson/geojsoncomponent.cpp + src/geojson/geojsonmanager.cpp + src/geojson/geojsonproperties.cpp + src/geojson/globegeometryfeature.cpp + src/geojson/globegeometryhelper.cpp src/tileprovider/defaulttileprovider.cpp src/tileprovider/imagesequencetileprovider.cpp src/tileprovider/singleimagetileprovider.cpp @@ -119,6 +129,11 @@ set(SHADER_FILES shaders/advanced_rings_vs.glsl shaders/advanced_rings_fs.glsl shaders/blending.glsl + shaders/geojson_fs.glsl + shaders/geojson_points_fs.glsl + shaders/geojson_points_gs.glsl + shaders/geojson_points_vs.glsl + shaders/geojson_vs.glsl shaders/globalrenderer_vs.glsl shaders/localrenderer_vs.glsl shaders/renderer_fs.glsl @@ -163,3 +178,8 @@ else (WIN32) target_link_libraries(openspace-module-globebrowsing PRIVATE ${GDAL_LIBRARY}) mark_as_advanced(GDAL_CONFIG GDAL_INCLUDE_DIR GDAL_LIBRARY) endif () # WIN32 + +begin_dependency("GEOS") +add_subdirectory(ext/geos SYSTEM) +target_link_libraries(openspace-module-globebrowsing PRIVATE geos) +end_dependency("GEOS") diff --git a/modules/globebrowsing/ext/geos b/modules/globebrowsing/ext/geos new file mode 160000 index 0000000000..fe36f23c2b --- /dev/null +++ b/modules/globebrowsing/ext/geos @@ -0,0 +1 @@ +Subproject commit fe36f23c2bd28c25669798ff041588e191aded67 diff --git a/modules/globebrowsing/globebrowsingmodule.cpp b/modules/globebrowsing/globebrowsingmodule.cpp index bc80c63ab8..0c40c6c39b 100644 --- a/modules/globebrowsing/globebrowsingmodule.cpp +++ b/modules/globebrowsing/globebrowsingmodule.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -98,6 +101,14 @@ namespace { 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", @@ -173,6 +184,9 @@ namespace { // [[codegen::verbatim(TileCacheSizeInfo.description)]] std::optional tileCacheSize; + // [[codegen::verbatim(DefaultGeoPointTextureInfo.description)]] + std::optional defaultGeoPointTexture; + // [[codegen::verbatim(MRFCacheEnabledInfo.description)]] std::optional mrfCacheEnabled [[codegen::key("MRFCacheEnabled")]]; @@ -187,10 +201,14 @@ 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); } @@ -200,6 +218,28 @@ void GlobeBrowsingModule::internalInitialize(const ghoul::Dictionary& dict) { 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); @@ -298,6 +338,9 @@ std::vector GlobeBrowsingModule::documentations() globebrowsing::TemporalTileProvider::Documentation(), globebrowsing::TileProviderByIndex::Documentation(), globebrowsing::TileProviderByLevel::Documentation(), + globebrowsing::GeoJsonManager::Documentation(), + globebrowsing::GeoJsonComponent::Documentation(), + globebrowsing::GeoJsonProperties::Documentation(), GlobeLabelsComponent::Documentation(), RingsComponent::Documentation(), ShadowComponent::Documentation() @@ -634,6 +677,14 @@ 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", @@ -651,7 +702,10 @@ scripting::LuaLibrary GlobeBrowsingModule::luaLibrary() const { codegen::lua::GetGeoPositionForCamera, codegen::lua::LoadWMSCapabilities, codegen::lua::RemoveWMSServer, - codegen::lua::CapabilitiesWMS + codegen::lua::CapabilitiesWMS, + codegen::lua::AddGeoJson, + codegen::lua::DeleteGeoJson, + codegen::lua::AddGeoJsonFromFile }, .scripts = { absPath("${MODULE_GLOBEBROWSING}/scripts/layer_support.lua") diff --git a/modules/globebrowsing/globebrowsingmodule.h b/modules/globebrowsing/globebrowsingmodule.h index 3902e5b663..783e027f2c 100644 --- a/modules/globebrowsing/globebrowsingmodule.h +++ b/modules/globebrowsing/globebrowsingmodule.h @@ -95,6 +95,9 @@ public: bool isMRFCachingEnabled() const; const std::string mrfCacheLocation() const; + bool hasDefaultGeoPointTexture() const; + std::string_view defaultGeoPointTexture() const; + protected: void internalInitialize(const ghoul::Dictionary&) override; @@ -113,6 +116,7 @@ private: properties::UIntProperty _tileCacheSizeMB; + properties::StringProperty _defaultGeoPointTexturePath; properties::BoolProperty _mrfCacheEnabled; properties::StringProperty _mrfCacheLocation; @@ -124,6 +128,8 @@ private: std::map _capabilitiesMap; std::multimap _urlList; + + bool _hasDefaultGeoPointTexture = false; }; } // namespace openspace diff --git a/modules/globebrowsing/globebrowsingmodule_lua.inl b/modules/globebrowsing/globebrowsingmodule_lua.inl index 9881a65fe0..21268feb04 100644 --- a/modules/globebrowsing/globebrowsingmodule_lua.inl +++ b/modules/globebrowsing/globebrowsingmodule_lua.inl @@ -530,6 +530,122 @@ getGeoPositionForCamera(bool useEyePosition = false) return res; } +/** + * Add a GeoJson layer specified by the given table to the globe specified by the + * 'globeName' argument + */ +[[codegen::luawrap]] void addGeoJson(std::string globeName, ghoul::Dictionary table) +{ + using namespace openspace; + using namespace globebrowsing; + + // Get the node and make sure it exists + SceneGraphNode* n = global::renderEngine->scene()->sceneGraphNode(globeName); + if (!n) { + throw ghoul::lua::LuaError("Unknown globe name: " + globeName); + } + + // Get the renderable globe + RenderableGlobe* globe = dynamic_cast(n->renderable()); + if (!globe) { + throw ghoul::lua::LuaError("Renderable is not a globe: " + globeName); + } + + // Get the dictionary defining the layer + globe->geoJsonManager().addGeoJsonLayer(table); +} + +/** + * Remove the GeoJson layer specified by the given table or string identifier from the + * globe specified by the 'globeName' argument + */ +[[codegen::luawrap]] void deleteGeoJson(std::string globeName, + std::variant tableOrIdentifier) +{ + using namespace openspace; + using namespace globebrowsing; + + // Get the node and make sure it exists + SceneGraphNode* n = global::renderEngine->scene()->sceneGraphNode(globeName); + if (!n) { + throw ghoul::lua::LuaError("Unknown globe name: " + globeName); + } + + // Get the renderable globe + RenderableGlobe* globe = dynamic_cast(n->renderable()); + if (!globe) { + throw ghoul::lua::LuaError("Renderable is not a globe: " + globeName); + } + + std::string identifier; + if (std::holds_alternative(tableOrIdentifier)) { + identifier = std::get(tableOrIdentifier); + } + else { + ghoul::Dictionary d = std::get(tableOrIdentifier); + if (!d.hasValue("Identifier")) { + throw ghoul::lua::LuaError( + "Table passed to deleteLayer does not contain an Identifier" + ); + } + identifier = d.value("Identifier"); + } + + globe->geoJsonManager().deleteLayer(identifier); +} + +/** + * Add a GeoJson layer from the given file name and add it to the current anchor node, + * if it is a globe. Note that you might have to increase the height offset for the + * added feature to be visible on the globe, if using a height map + */ +[[codegen::luawrap]] void addGeoJsonFromFile(std::string filename, + std::optional name) +{ + using namespace openspace; + using namespace globebrowsing; + + std::filesystem::path path = absPath(filename); + if (!std::filesystem::is_regular_file(path)) { + throw ghoul::lua::LuaError(fmt::format( + "Could not find the provided file: '{}'", filename + )); + } + + if (path.extension() != ".geojson") { + throw ghoul::lua::LuaError(fmt::format( + "Unexpected file type: '{}'. Expected '.geojson' file", filename + )); + } + + SceneGraphNode* n = global::renderEngine->scene()->sceneGraphNode( + global::navigationHandler->anchorNode()->identifier() + ); + if (!n) { + throw ghoul::lua::LuaError("Invalid anchor node"); + } + + RenderableGlobe* globe = dynamic_cast(n->renderable()); + if (!globe) { + throw ghoul::lua::LuaError( + "Current anchor is not a globe (Expected 'RenderableGlobe')" + ); + } + + // Make a minimal dictionary to represent the geojson component + ghoul::Dictionary d; + + std::string identifier = makeIdentifier(name.value_or(path.stem().string())); + d.setValue("Identifier", identifier); + d.setValue("File", path.string()); + if (name.has_value()) { + d.setValue("Name", *name); + } + + // Get the dictionary defining the layer + globe->geoJsonManager().addGeoJsonLayer(d); +} + #include "globebrowsingmodule_lua_codegen.cpp" } // namespace diff --git a/modules/globebrowsing/shaders/geojson_fs.glsl b/modules/globebrowsing/shaders/geojson_fs.glsl new file mode 100644 index 0000000000..7cb33a3825 --- /dev/null +++ b/modules/globebrowsing/shaders/geojson_fs.glsl @@ -0,0 +1,76 @@ +/***************************************************************************************** + * * + * 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 "fragment.glsl" + +in float vs_depth; +flat in vec3 vs_normal; +in vec4 vs_positionViewSpace; + +uniform vec3 color; +uniform float opacity; + +uniform float ambientIntensity = 0.2; +uniform float diffuseIntensity = 0.8; +uniform bool performShading = true; + +uniform unsigned int nLightSources; +uniform vec3 lightDirectionsViewSpace[8]; +uniform float lightIntensities[8]; + +const vec3 LightColor = vec3(1.0); + +Fragment getFragment() { + Fragment frag; + + if (opacity == 0.0) { + discard; + } + frag.color = vec4(color, opacity); + + // Simple diffuse phong shading based on light sources + if (performShading && nLightSources > 0) { + // @TODO: Fix faulty triangle normals. This should not have to be inverted + vec3 n = -normalize(vs_normal); + + // Ambient color + vec3 shadedColor = ambientIntensity * color; + + for (int i = 0; i < nLightSources; ++i) { + vec3 l = lightDirectionsViewSpace[i]; + + // Diffuse + vec3 diffuseColor = diffuseIntensity * max(dot(n,l), 0.0) * color; + + // Light contribution + shadedColor += lightIntensities[i] * (LightColor * diffuseColor); + } + frag.color.xyz = shadedColor; + } + + frag.depth = vs_depth; + frag.gPosition = vs_positionViewSpace; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + return frag; +} diff --git a/modules/globebrowsing/shaders/geojson_points_fs.glsl b/modules/globebrowsing/shaders/geojson_points_fs.glsl new file mode 100644 index 0000000000..8ec728cb34 --- /dev/null +++ b/modules/globebrowsing/shaders/geojson_points_fs.glsl @@ -0,0 +1,62 @@ +/***************************************************************************************** + * * + * 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 "fragment.glsl" + +flat in float vs_screenSpaceDepth; +in vec4 vs_positionViewSpace; +flat in vec3 vs_normal; // TODO: not needed for shading, remove somehow +in vec2 texCoord; + +uniform sampler2D pointTexture; +uniform bool hasTexture; +uniform vec3 color; +uniform float opacity; + +// Can be used to preserve the whites in a point texture +bool preserveWhite = true; + +Fragment getFragment() { + Fragment frag; + + if (hasTexture) { + frag.color = texture(pointTexture, texCoord); + if (!preserveWhite || frag.color.r * frag.color.g * frag.color.b < 0.95) { + frag.color.rgb *= color; + } + frag.color.a *= opacity; + } + else { + frag.color = vec4(color * vs_normal, opacity); + } + + if (frag.color.a < 0.01) { + discard; + } + + frag.depth = vs_screenSpaceDepth; + frag.gPosition = vs_positionViewSpace; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + return frag; +} diff --git a/modules/globebrowsing/shaders/geojson_points_gs.glsl b/modules/globebrowsing/shaders/geojson_points_gs.glsl new file mode 100644 index 0000000000..9eda63fa98 --- /dev/null +++ b/modules/globebrowsing/shaders/geojson_points_gs.glsl @@ -0,0 +1,158 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "PowerScaling/powerScalingMath.hglsl" + +layout(points) in; +flat in vec3 normal[]; // Point normals correspond to globe out direction, model space +flat in float dynamicHeight[]; + +layout(triangle_strip, max_vertices = 4) out; +out vec2 texCoord; +flat out float vs_screenSpaceDepth; +out vec4 vs_positionViewSpace; +flat out vec3 vs_normal; + +// General settings +uniform dmat4 modelTransform; +uniform dmat4 viewTransform; +uniform dmat4 projectionTransform; + +uniform float heightOffset; +uniform bool useHeightMapData; + +// Camera information +uniform vec3 cameraUp; +uniform vec3 cameraRight; +uniform dvec3 cameraPosition; // world coordinates +uniform vec3 cameraLookUp; + +// Render mode +uniform int renderMode; +// OBS! Keep in sync with option property options +const int RenderOptionCameraDir = 0; +const int RenderOptionCameraPos = 1; +const int RenderOptionGlobeNormal = 2; +const int RenderOptionGlobeSurface = 3; + +uniform float pointSize; +uniform float textureWidthFactor; + +// If false, use the center +uniform bool useBottomAnchorPoint = true; + +const vec2 corners[4] = vec2[4]( + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0) +); + +void main() { + vec4 pos = gl_in[0].gl_Position; + vs_normal = normal[0]; + dvec4 dpos = dvec4(dvec3(pos.xyz), 1.0); + + // Offset position based on height information + if (length(pos.xyz) > 0) { + dvec3 outDirection = normalize(dvec3(dpos)); + float height = heightOffset; + if (useHeightMapData) { + height += dynamicHeight[0]; + } + dpos += dvec4(outDirection * double(height), 0.0); + } + // World coordinates + dpos = modelTransform * dpos; + vec3 worldNormal = normalize(mat3(modelTransform) * vs_normal); + + // Set up and right directions based on render mode. + // renderMode 0 is default + vec3 right = cameraRight; + vec3 up = cameraUp; + vec3 cameraToPosDir = vec3(normalize(cameraPosition - dpos.xyz)); + + // Update right and up based on render mode + if (renderMode == RenderOptionCameraPos) { + right = normalize(cross(cameraLookUp, cameraToPosDir)); + up = normalize(cross(cameraToPosDir, right)); + } + else if (renderMode == RenderOptionGlobeNormal) { + up = worldNormal; + right = normalize(cross(up, cameraToPosDir)); + } + else if (renderMode == RenderOptionGlobeSurface) { + // Compute up to be orthogonal to globe normal and camera right direction + up = normalize(cross(worldNormal, right)); + // Recompute right to be orthognal to globe normal + right = cross(up, worldNormal); + } + + dvec4 scaledRight = pointSize * dvec4(right, 0.0) * 0.5; + dvec4 scaledUp = pointSize * dvec4(up, 0.0) * 0.5; + + dmat4 cameraViewProjectionMatrix = projectionTransform * viewTransform; + + vec4 dposClip = vec4(cameraViewProjectionMatrix * dpos); + vec4 scaledRightClip = textureWidthFactor * vec4(cameraViewProjectionMatrix * scaledRight); + vec4 scaledUpClip = vec4(cameraViewProjectionMatrix * scaledUp); + + // Place anchor point at the bottom + vec4 bottomLeft = z_normalization(dposClip - scaledRightClip); + vec4 bottomRight = z_normalization(dposClip + scaledRightClip); + vec4 topRight = z_normalization(dposClip + 2 * scaledUpClip + scaledRightClip); + vec4 topLeft = z_normalization(dposClip + 2 * scaledUpClip - scaledRightClip); + + if (!useBottomAnchorPoint) { + // Place anchor point at the center + bottomLeft = z_normalization(dposClip - scaledRightClip - scaledUpClip); + bottomRight = z_normalization(dposClip + scaledRightClip - scaledUpClip); + topRight = z_normalization(dposClip + scaledUpClip + scaledRightClip); + topLeft = z_normalization(dposClip + scaledUpClip - scaledRightClip); + } + + vs_screenSpaceDepth = bottomLeft.w; + vs_positionViewSpace = vec4(viewTransform * dpos); + + // Build primitive + texCoord = corners[0]; + gl_Position = bottomLeft; + EmitVertex(); + + texCoord = corners[1]; + gl_Position = bottomRight; + EmitVertex(); + + texCoord = corners[3]; + gl_Position = topLeft; + EmitVertex(); + + texCoord = corners[2]; + gl_Position = topRight; + EmitVertex(); + + EndPrimitive(); +} diff --git a/modules/globebrowsing/shaders/geojson_points_vs.glsl b/modules/globebrowsing/shaders/geojson_points_vs.glsl new file mode 100644 index 0000000000..65f2b13780 --- /dev/null +++ b/modules/globebrowsing/shaders/geojson_points_vs.glsl @@ -0,0 +1,38 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +layout(location = 0) in vec3 in_position; +layout(location = 1) in vec3 in_normal; +layout(location = 2) in float in_height; + +out vec3 normal; +out float dynamicHeight; + +void main() { + gl_Position = vec4(in_position, 1.0); + normal = in_normal; + dynamicHeight = in_height; +} diff --git a/modules/globebrowsing/shaders/geojson_vs.glsl b/modules/globebrowsing/shaders/geojson_vs.glsl new file mode 100644 index 0000000000..f414fac371 --- /dev/null +++ b/modules/globebrowsing/shaders/geojson_vs.glsl @@ -0,0 +1,64 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +layout(location = 0) in vec3 in_position; +layout(location = 1) in vec3 in_normal; +layout(location = 2) in float in_height; + +out float vs_depth; +out vec3 vs_normal; +out vec4 vs_positionViewSpace; + +uniform dmat4 modelTransform; +uniform dmat4 viewTransform; +uniform dmat4 projectionTransform; +uniform mat3 normalTransform; + +uniform float heightOffset; +uniform bool useHeightMapData; + +void main() { + dvec4 modelPos = dvec4(in_position, 1.0); + + // Offset model pos based on height info + if (length(in_position) > 0) { + dvec3 outDirection = normalize(dvec3(in_position)); + float height = heightOffset; + if (useHeightMapData) { + height += in_height; + } + modelPos += dvec4(outDirection * double(height), 0.0); + } + + vs_positionViewSpace = vec4(viewTransform * modelTransform * modelPos); + vec4 positionScreenSpace = vec4(projectionTransform * vs_positionViewSpace); + vs_depth = positionScreenSpace.w; + vs_normal = normalize(normalTransform * in_normal); + gl_Position = positionScreenSpace; + + // Set z to 0 to disable near and far plane, unique handling for perspective in space + gl_Position.z = 0.0; +} diff --git a/modules/globebrowsing/src/geojson/geojsoncomponent.cpp b/modules/globebrowsing/src/geojson/geojsoncomponent.cpp new file mode 100644 index 0000000000..aeeed1b655 --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsoncomponent.cpp @@ -0,0 +1,797 @@ +/***************************************************************************************** + * * + * 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 + +namespace geos_nlohmann = nlohmann; +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "GeoJsonComponent"; + + constexpr std::string_view KeyIdentifier = "Identifier"; + constexpr std::string_view KeyName = "Name"; + constexpr std::string_view KeyDesc = "Description"; + + constexpr openspace::properties::Property::PropertyInfo EnabledInfo = { + "Enabled", + "Is Enabled", + "This setting determines whether this object will be visible or not", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo FileInfo = { + "File", + "File", + "Path to the GeoJSON file to base the rendering on", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo HeightOffsetInfo = { + "HeightOffset", + "Height Offset", + "A height offset value, in meters. Useful for moving a feature closer to or " + "farther away from the surface", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo CoordinateOffsetInfo = { + "CoordinateOffset", + "Geographic Coordinate Offset", + "A latitude and longitude offset value, in decimal degrees. Can be used to " + "move the object on the surface and correct potential mismatches with other " + "renderings. Note that changing it during runtime leads to all positions being " + "recomputed", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo DrawWireframeInfo = { + "DrawWireframe", + "Wireframe", + "If true, draw the wire frame of the polygons. Used for testing and to " + "investigate tessellation results", + openspace::properties::Property::Visibility::Developer + }; + + constexpr openspace::properties::Property::PropertyInfo PreventHeightUpdateInfo = { + "PreventHeightUpdate", + "Prevent Update From Heightmap", + "If true, the polygon mesh will not be automatically updated based on the " + "heightmap, even if the 'RelativeToGround' altitude option is set and the " + "heightmap updates. The data can still be force updated", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo ForceUpdateHeightDataInfo = { + "ForceUpdateHeightData", + "Force Update Height Data", + "Triggering this leads to a recomputation of the heights based on the globe " + "height map value at the geometry's positions", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo PointRenderModeInfo = { + "PointRenderMode", + "Points Aligned to", + "Decides how the billboards for the points should be rendered in terms of up " + "direction and whether the plane should face the camera. See details on the " + "different options in the wiki", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo FlyToFeatureInfo = { + "FlyToFeature", + "Fly To Feature", + "Triggering this leads to the camera flying to a position that show the GeoJson " + "feature. The flight will account for any lat, long or height offset", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo CentroidCoordinateInfo = { + "CentroidCoordinate", + "Centroid Coordinate", + "The lat long coordinate of the centroid position of the read geometry. Note " + "that this value does not incude the offset", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo BoundingBoxInfo = { + "BoundingBox", + "Bounding Box", + "The lat long coordinates of the lower and upper corner of the bounding box of " + "the read geometry. Note that this value does not incude the offset", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo PointSizeScaleInfo = { + "PointSizeScale", + "Point Size Scale", + "An extra scale value that can be used to increase or decrease the scale of any " + "rendered points in the component, even if a value is set from the GeoJson file", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo LineWidthScaleInfo = { + "LineWidthScale", + "Line Width Scale", + "An extra scale value that can be used to increase or decrease the width of any " + "rendered lines in the component, even if a value is set from the GeoJson file. " + "Note that there is a max limit for how wide lines can be.", + openspace::properties::Property::Visibility::NoviceUser + }; + + struct [[codegen::Dictionary(GeoJsonComponent)]] Parameters { + // The unique identifier for this layer. May not contain '.' or spaces + std::string identifier; + + // A human-readable name for the user interface. If this is omitted, the + // identifier is used instead + std::optional name; + + // [[codegen::verbatim(EnabledInfo.description)]] + std::optional enabled; + + // The opacity of the component + std::optional opacity [[codegen::inrange(0.0, 1.0)]]; + + // A human-readable description of the layer to be used in informational texts + // presented to the user + std::optional description; + + // If true, ignore any height values that are given in the file. Coordinates with + // three values will then be treated as coordinates with only two values + std::optional ignoreHeights; + + // [[codegen::verbatim(PreventHeightUpdateInfo.description)]] + std::optional preventHeightUpdate; + + // [[codegen::verbatim(FileInfo.description)]] + std::filesystem::path file; + + // [[codegen::verbatim(HeightOffsetInfo.description)]] + std::optional heightOffset; + + // [[codegen::verbatim(PointSizeScaleInfo.description)]] + std::optional pointSizeScale; + + // [[codegen::verbatim(LineWidthScaleInfo.description)]] + std::optional lineWidthScale; + + // [[codegen::verbatim(CoordinateOffsetInfo.description)]] + std::optional coordinateOffset; + + enum class [[codegen::map(openspace::globebrowsing::GlobeGeometryFeature::PointRenderMode)]] PointRenderMode { + AlignToCameraDir [[codegen::key("Camera Direction")]], + AlignToCameraPos [[codegen::key("Camera Position")]], + AlignToGlobeNormal [[codegen::key("Globe Normal")]], + AlignToGlobeSurface [[codegen::key("Globe Surface")]] + }; + // [[codegen::verbatim(PointRenderModeInfo.description)]] + std::optional pointRenderMode; + + // [[codegen::verbatim(DrawWireframeInfo.description)]] + std::optional drawWireframe; + + // These properties will be used as default values for the geoJson rendering, + // meaning that they will be used when there is no value given for the + // individual geoJson features + std::optional defaultProperties + [[codegen::reference("globebrowsing_geojsonproperties")]]; + + // A list of light sources that this object should accept light from + std::optional> lightSources + [[codegen::reference("core_light_source")]]; + }; +#include "geojsoncomponent_codegen.cpp" +} // namespace + +namespace openspace::globebrowsing { + +documentation::Documentation GeoJsonComponent::Documentation() { + return codegen::doc("globebrowsing_geojsoncomponent"); +} + +GeoJsonComponent::SubFeatureProps::SubFeatureProps( + properties::PropertyOwner::PropertyOwnerInfo info) + : properties::PropertyOwner(info) + , enabled(EnabledInfo, true) + , flyToFeature(FlyToFeatureInfo) + , centroidLatLong( + CentroidCoordinateInfo, + glm::vec2(0.f), + glm::vec2(-90.f, -180.f), + glm::vec2(90.f, 180.f) + ) + , boundingboxLatLong( + BoundingBoxInfo, + glm::vec4(0.f), + glm::vec4(-90.f, -180.f, -90.f, -180.f), + glm::vec4(90.f, 180.f, 90.f, 180.f) + ) +{ + _opacity.setVisibility(openspace::properties::Property::Visibility::AdvancedUser); + addProperty(Fadeable::_opacity); + addProperty(Fadeable::_fade); + + addProperty(enabled); + + addProperty(flyToFeature); + + centroidLatLong.setReadOnly(true); + addProperty(centroidLatLong); + + boundingboxLatLong.setReadOnly(true); + addProperty(boundingboxLatLong); +} + +GeoJsonComponent::GeoJsonComponent(const ghoul::Dictionary& dictionary, + RenderableGlobe& globe) + : properties::PropertyOwner({ + dictionary.value(KeyIdentifier), + dictionary.hasKey(KeyName) ? dictionary.value(KeyName) : "", + dictionary.hasKey(KeyDesc) ? dictionary.value(KeyDesc) : "" + }) + , _enabled(EnabledInfo, true) + , _globeNode(globe) + , _geoJsonFile(FileInfo) + , _heightOffset(HeightOffsetInfo, 10.f, -1e12f, 1e12f) + , _latLongOffset( + CoordinateOffsetInfo, + glm::vec2(0.f), + glm::vec2(-90.0), + glm::vec2(90.f) + ) + , _pointSizeScale(PointSizeScaleInfo, 1.f, 0.01f, 100.f) + , _lineWidthScale(LineWidthScaleInfo, 1.f, 0.01f, 10.f) + , _drawWireframe(DrawWireframeInfo, false) + , _preventUpdatesFromHeightMap(PreventHeightUpdateInfo, false) + , _forceUpdateHeightData(ForceUpdateHeightDataInfo) + , _lightSourcePropertyOwner({ "LightSources", "Light Sources" }) + , _featuresPropertyOwner({ "Features", "Features" }) + , _pointRenderModeOption( + PointRenderModeInfo, + properties::OptionProperty::DisplayType::Dropdown + ) + , _centerLatLong( + CentroidCoordinateInfo, + glm::vec2(0.f), + glm::vec2(-90.f, -180.f), + glm::vec2(90.f, 180.f) + ) + , _flyToFeature(FlyToFeatureInfo) +{ + const Parameters p = codegen::bake(dictionary); + + _enabled = p.enabled.value_or(_enabled); + addProperty(_enabled); + + _opacity = p.opacity.value_or(_opacity); + addProperty(_opacity); + addProperty(_fade); + + _geoJsonFile = p.file.string(); + _geoJsonFile.setReadOnly(true); + addProperty(_geoJsonFile); + + _ignoreHeightsFromFile = p.ignoreHeights.value_or(_ignoreHeightsFromFile); + + const float minGlobeRadius = static_cast( + _globeNode.ellipsoid().minimumRadius() + ); + + _heightOffset = p.heightOffset.value_or(_heightOffset); + _heightOffset.onChange([this]() { _heightOffsetIsDirty = true; }); + constexpr float MinRadiusFactor = -0.9f; + constexpr float MaxRadiusFactor = 5.f; + _heightOffset.setMinValue(MinRadiusFactor * minGlobeRadius); + _heightOffset.setMaxValue(MaxRadiusFactor * minGlobeRadius); + addProperty(_heightOffset); + + _latLongOffset = p.coordinateOffset.value_or(_latLongOffset); + _latLongOffset.onChange([this]() { _dataIsDirty = true; }); + addProperty(_latLongOffset); + + _pointSizeScale = p.pointSizeScale.value_or(_pointSizeScale); + addProperty(_pointSizeScale); + + _lineWidthScale = p.lineWidthScale.value_or(_lineWidthScale); + addProperty(_lineWidthScale); + + if (p.defaultProperties.has_value()) { + _defaultProperties.createFromDictionary(*p.defaultProperties, _globeNode); + } + addPropertySubOwner(_defaultProperties); + + _defaultProperties.pointTexture.onChange([this]() { + std::filesystem::path texturePath = _defaultProperties.pointTexture.value(); + // Not ethat an empty texture is also valid => use default texture from module + if (std::filesystem::is_regular_file(texturePath) || texturePath.empty()) { + _textureIsDirty = true; + } + else { + LERROR(fmt::format( + "Provided texture file does not exist: '{}'", + _defaultProperties.pointTexture + )); + } + }); + + _defaultProperties.tessellation.enabled.onChange([this]() { _dataIsDirty = true; }); + _defaultProperties.tessellation.useLevel.onChange([this]() { _dataIsDirty = true; }); + _defaultProperties.tessellation.level.onChange([this]() { _dataIsDirty = true; }); + _defaultProperties.tessellation.distance.onChange([this]() { + _dataIsDirty = true; + }); + + _forceUpdateHeightData.onChange([this]() { + for (GlobeGeometryFeature& f : _geometryFeatures) { + f.updateHeightsFromHeightMap(); + } + }); + addProperty(_forceUpdateHeightData); + + _preventUpdatesFromHeightMap = + p.preventHeightUpdate.value_or(_preventUpdatesFromHeightMap); + addProperty(_preventUpdatesFromHeightMap); + + _drawWireframe = p.drawWireframe.value_or(_drawWireframe); + addProperty(_drawWireframe); + + using PointRenderMode = GlobeGeometryFeature::PointRenderMode; + _pointRenderModeOption.addOptions({ + { static_cast(PointRenderMode::AlignToCameraDir), "Camera Direction"}, + { static_cast(PointRenderMode::AlignToCameraPos), "Camera Position"}, + { static_cast(PointRenderMode::AlignToGlobeNormal), "Globe Normal"}, + { static_cast(PointRenderMode::AlignToGlobeSurface), "Globe Surface"} + }); + if (p.pointRenderMode.has_value()) { + _pointRenderModeOption = + static_cast(codegen::map(*p.pointRenderMode)); + } + addProperty(_pointRenderModeOption); + + _centerLatLong.setReadOnly(true); + addProperty(_centerLatLong); + + _flyToFeature.onChange([this]() { flyToFeature(); }); + addProperty(_flyToFeature); + + readFile(); + + if (p.lightSources.has_value()) { + std::vector lightsources = *p.lightSources; + + for (const ghoul::Dictionary& lsDictionary : lightsources) { + std::unique_ptr lightSource = + LightSource::createFromDictionary(lsDictionary); + _lightSourcePropertyOwner.addPropertySubOwner(lightSource.get()); + _lightSources.push_back(std::move(lightSource)); + } + } + else { + // If no light source provided, add a deafult light source from the camera + using namespace std::string_literals; + ghoul::Dictionary defaultLightSourceDict; + defaultLightSourceDict.setValue("Identifier", "Camera"s); + defaultLightSourceDict.setValue("Type", "CameraLightSource"s); + defaultLightSourceDict.setValue("Intensity", 1.0); + _lightSources.push_back( + LightSource::createFromDictionary(defaultLightSourceDict) + ); + _lightSourcePropertyOwner.addPropertySubOwner(_lightSources.back().get()); + } + addPropertySubOwner(_lightSourcePropertyOwner); + addPropertySubOwner(_featuresPropertyOwner); +} + +bool GeoJsonComponent::enabled() const { + return _enabled; +} + +void GeoJsonComponent::initialize() { + ZoneScoped; + + for (const std::unique_ptr& ls : _lightSources) { + ls->initialize(); + } +} + +void GeoJsonComponent::initializeGL() { + ZoneScoped; + + _linesAndPolygonsProgram = global::renderEngine->buildRenderProgram( + "GeoLinesAndPolygonProgram", + absPath("${MODULE_GLOBEBROWSING}/shaders/geojson_vs.glsl"), + absPath("${MODULE_GLOBEBROWSING}/shaders/geojson_fs.glsl") + ); + + _pointsProgram = global::renderEngine->buildRenderProgram( + "GeoPointsProgram", + absPath("${MODULE_GLOBEBROWSING}/shaders/geojson_points_vs.glsl"), + absPath("${MODULE_GLOBEBROWSING}/shaders/geojson_points_fs.glsl"), + absPath("${MODULE_GLOBEBROWSING}/shaders/geojson_points_gs.glsl") + ); + + for (GlobeGeometryFeature& g : _geometryFeatures) { + g.initializeGL(_pointsProgram.get(), _linesAndPolygonsProgram.get()); + } +} + +void GeoJsonComponent::deinitializeGL() { + for (GlobeGeometryFeature& g : _geometryFeatures) { + g.deinitializeGL(); + } + + global::renderEngine->removeRenderProgram(_linesAndPolygonsProgram.get()); + _linesAndPolygonsProgram = nullptr; + + global::renderEngine->removeRenderProgram(_pointsProgram.get()); + _pointsProgram = nullptr; +} + +bool GeoJsonComponent::isReady() const { + bool isReady = std::all_of( + std::begin(_geometryFeatures), + std::end(_geometryFeatures), + std::mem_fn(&GlobeGeometryFeature::isReady) + ); + return isReady && _linesAndPolygonsProgram && _pointsProgram; +} + +void GeoJsonComponent::render(const RenderData& data) { + if (!_enabled || !isVisible()) { + return; + } + + // @TODO (2023-03-17, emmbr): Once the light source for the globe can be configured, + // this code should use the same light source as the globe + _lightsourceRenderData.updateBasedOnLightSources(data, _lightSources); + + // Change GL state: + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + if (_drawWireframe) { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + + using PointRenderMode = GlobeGeometryFeature::PointRenderMode; + PointRenderMode pointRenderMode = + static_cast(_pointRenderModeOption.value()); + + // Compose extra data from relevant properties to pass to the individual features + const GlobeGeometryFeature::ExtraRenderData extraRenderdata = { + _pointSizeScale, + _lineWidthScale, + pointRenderMode, + _lightsourceRenderData + }; + + // Do two render passes, to properly render opacity of overlaying objects + for (int renderPass = 0; renderPass < 2; ++renderPass) { + for (size_t i = 0; i < _geometryFeatures.size(); ++i) { + if (_features[i]->enabled && _features[i]->isVisible()) { + _geometryFeatures[i].render( + data, + renderPass, + opacity() * _features[i]->opacity(), + extraRenderdata + ); + } + } + } + + if (_drawWireframe) { + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + + glBindVertexArray(0); + + // Restore GL State + global::renderEngine->openglStateCache().resetPolygonAndClippingState(); + global::renderEngine->openglStateCache().resetBlendState(); + global::renderEngine->openglStateCache().resetDepthState(); + global::renderEngine->openglStateCache().resetLineState(); +} + +void GeoJsonComponent::update() { + if (!_enabled || !isVisible()) { + return; + } + + glm::vec3 offsets = glm::vec3(_latLongOffset.value(), _heightOffset); + + for (size_t i = 0; i < _geometryFeatures.size(); ++i) { + if (!_features[i]->enabled) { + continue; + } + GlobeGeometryFeature& g = _geometryFeatures[i]; + + if (_dataIsDirty || _heightOffsetIsDirty) { + g.setOffsets(offsets); + } + + if (_textureIsDirty) { + g.updateTexture(); + } + + g.update(_dataIsDirty, _preventUpdatesFromHeightMap); + } + + _textureIsDirty = false; + _dataIsDirty = false; +} + +void GeoJsonComponent::readFile() { + std::ifstream file(_geoJsonFile); + + if (!file.good()) { + LERROR(fmt::format("Failed to open GeoJSON file: {}", _geoJsonFile)); + return; + } + + _geometryFeatures.clear(); + + std::string content( + (std::istreambuf_iterator(file)), + (std::istreambuf_iterator()) + ); + + // Parse GeoJSON string into GeoJSON objects + using namespace geos::io; + GeoJSONReader reader; + + try { + GeoJSONFeatureCollection fc = reader.readFeatures(content); + + for (const GeoJSONFeature& feature : fc.getFeatures()) { + parseSingleFeature(feature); + } + + if (_geometryFeatures.empty()) { + LWARNING(fmt::format( + "No GeoJson features could be successfully created for GeoJson layer " + "with identifier '{}'. Disabling layer.", identifier() + )); + _enabled = false; + } + } + catch (const geos::util::GEOSException& e) { + LERROR(fmt::format( + "Error creating GeoJson layer with identifier '{}'. Problem reading " + "GeoJson file '{}'. Error: '{}'", identifier(), _geoJsonFile, e.what() + )); + } + + computeMainFeatureMetaPropeties(); +} + +void GeoJsonComponent::parseSingleFeature(const geos::io::GeoJSONFeature& feature) { + // Read the geometry + const geos::geom::Geometry* geom = feature.getGeometry(); + ghoul_assert(geom, "No geometry found"); + + // Read the properties + GeoJsonOverrideProperties propsFromFile = propsFromGeoJson(feature); + + std::vector geomsToAdd; + if (geom->isPuntal()) { + // If points, handle all point features as one feature, even multi-points + geomsToAdd = { geom }; + } + else { + size_t nGeom = geom->getNumGeometries(); + geomsToAdd.reserve(nGeom); + for (size_t i = 0; i < nGeom; ++i) { + geomsToAdd.push_back(geom->getGeometryN(i)); + } + } + + // Split other collection features into multiple individual rendered components + + for (const geos::geom::Geometry* geometry : geomsToAdd) { + const int index = static_cast(_geometryFeatures.size()); + try { + GlobeGeometryFeature g(_globeNode, _defaultProperties, propsFromFile); + g.createFromSingleGeosGeometry(geometry, index, _ignoreHeightsFromFile); + g.initializeGL(_pointsProgram.get(), _linesAndPolygonsProgram.get()); + _geometryFeatures.push_back(std::move(g)); + + std::string name = _geometryFeatures.back().key(); + properties::PropertyOwner::PropertyOwnerInfo info = { + makeIdentifier(name), + name + // @TODO: Use description from file, if any + }; + _features.push_back(std::make_unique(info)); + + addMetaPropertiesToFeature(*_features.back(), index, geometry); + + _featuresPropertyOwner.addPropertySubOwner(_features.back().get()); + } + catch (const ghoul::MissingCaseException&) { + LERROR(fmt::format( + "Error creating GeoJson layer with identifier '{}'. Problem reading " + "feature {} in GeoJson file '{}'.", identifier(), index, _geoJsonFile + )); + // Do nothing + } + } +} + +void GeoJsonComponent::addMetaPropertiesToFeature(SubFeatureProps& feature, int index, + const geos::geom::Geometry* geometry) +{ + std::unique_ptr centroid = geometry->getCentroid(); + geos::geom::CoordinateXY centroidCoord = *centroid->getCoordinate(); + glm::vec2 centroidLatLong = glm::vec2(centroidCoord.y, centroidCoord.x); + feature.centroidLatLong = centroidLatLong; + + std::unique_ptr boundingbox = geometry->getEnvelope(); + std::unique_ptr coords = boundingbox->getCoordinates(); + glm::vec4 boundingboxLatLong; + if (boundingbox->isRectangle()) { + // A rectangle has 5 coordinates, where the first and third are two corners + boundingboxLatLong = glm::vec4( + (*coords)[0].y, + (*coords)[0].x, + (*coords)[2].y, + (*coords)[2].x + ); + } + else { + // Invalid boundingbox. Can happen e.g. for single points. + // Just add a degree to every direction from the centroid + boundingboxLatLong = glm::vec4( + centroidLatLong.x - 1.f, + centroidLatLong.y - 1.f, + centroidLatLong.x + 1.f, + centroidLatLong.y + 1.f + ); + } + + feature.boundingboxLatLong = boundingboxLatLong; + + // Compute the diagonal distance of the bounding box + Geodetic2 pos0 = { + glm::radians(boundingboxLatLong.x), + glm::radians(boundingboxLatLong.y) + }; + + Geodetic2 pos1 = { + glm::radians(boundingboxLatLong.z), + glm::radians(boundingboxLatLong.w) + }; + feature.boundingBoxDiagonal = static_cast( + std::abs(_globeNode.ellipsoid().greatCircleDistance(pos0, pos1)) + ); + + feature.flyToFeature.onChange([this, index]() { flyToFeature(index); }); +} + +void GeoJsonComponent::computeMainFeatureMetaPropeties() { + if (_features.empty()) { + return; + } + + glm::vec2 bboxLowerCorner = { + _features.front()->boundingboxLatLong.value().x, + _features.front()->boundingboxLatLong.value().y + }; + glm::vec2 bboxUpperCorner = { + _features.front()->boundingboxLatLong.value().z, + _features.front()->boundingboxLatLong.value().w + }; + + for (const std::unique_ptr& f : _features) { + // Update bbox corners + if (f->boundingboxLatLong.value().x < bboxLowerCorner.x) { + bboxLowerCorner.x = f->boundingboxLatLong.value().x; + } + if (f->boundingboxLatLong.value().y < bboxLowerCorner.y) { + bboxLowerCorner.y = f->boundingboxLatLong.value().y; + } + if (f->boundingboxLatLong.value().z > bboxUpperCorner.x) { + bboxUpperCorner.x = f->boundingboxLatLong.value().z; + } + if (f->boundingboxLatLong.value().w > bboxUpperCorner.y) { + bboxUpperCorner.y = f->boundingboxLatLong.value().w; + } + } + + // Identify the bounding box midpoints + _centerLatLong = 0.5f * (bboxLowerCorner + bboxUpperCorner); + + // Compute the diagonal distance (size) of the bounding box + Geodetic2 pos0 = { + glm::radians(bboxLowerCorner.x), + glm::radians(bboxLowerCorner.y) + }; + + Geodetic2 pos1 = { + glm::radians(bboxUpperCorner.x), + glm::radians(bboxUpperCorner.y) + }; + _bboxDiagonalSize = static_cast( + std::abs(_globeNode.ellipsoid().greatCircleDistance(pos0, pos1)) + ); +} + +void GeoJsonComponent::flyToFeature(std::optional index) const { + // General size properties + float diagonal = _bboxDiagonalSize; + float centroidLat = _centerLatLong.value().x; + float centroidLon = _centerLatLong.value().y; + + if (index.has_value()) { + const SubFeatureProps* f = _features[*index].get(); + diagonal = f->boundingBoxDiagonal; + centroidLat = f->centroidLatLong.value().x; + centroidLon = f->centroidLatLong.value().y; + } + + // Compute a good distance to travel to based on the feature's size. + // Assumes 80 degree FOV + constexpr float Angle = glm::radians(40.f); + float d = diagonal / glm::tan(Angle); + d += _heightOffset; + + float lat = centroidLat + _latLongOffset.value().x; + float lon = centroidLon + _latLongOffset.value().y; + + global::scriptEngine->queueScript( + fmt::format( + "openspace.globebrowsing.flyToGeo(\"{}\", {}, {}, {})", + _globeNode.owner()->identifier(), lat, lon, d + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); +} + +} // namespace openspace::globebrowsing diff --git a/modules/globebrowsing/src/geojson/geojsoncomponent.h b/modules/globebrowsing/src/geojson/geojsoncomponent.h new file mode 100644 index 0000000000..d578fb9394 --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsoncomponent.h @@ -0,0 +1,163 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONCOMPONENT___H__ +#define __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONCOMPONENT___H__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace { + struct RenderData; + class LightSource; + namespace documentation { struct Documentation; } + namespace rendering::helper { struct VertexXYZNormal; } +} // namespace::openspace + +namespace ghoul::opengl { class ProgramObject; } +namespace geos::io { class GeoJSONFeature; } + +namespace openspace::globebrowsing { + +class RenderableGlobe; + +/** + * A component representing a collection of globe geometry features, whose details + * are read from a GeoJson file + */ +class GeoJsonComponent : public properties::PropertyOwner, public Fadeable { +public: + GeoJsonComponent(const ghoul::Dictionary& dictionary, RenderableGlobe& globe); + + void initialize(); + void initializeGL(); + void deinitializeGL(); + + bool isReady() const; + bool enabled() const; + + void render(const RenderData& data); + void update(); + + static documentation::Documentation Documentation(); + +private: + /** + * Small helper class whose purpose is to encapsulate properties related to a + * specific geomoetry feature, and allow things like flying to or fadin out + * individual subfeatures + */ + class SubFeatureProps : public properties::PropertyOwner, public Fadeable { + public: + SubFeatureProps(properties::PropertyOwner::PropertyOwnerInfo info); + + properties::BoolProperty enabled; + properties::Vec2Property centroidLatLong; + properties::Vec4Property boundingboxLatLong; + properties::TriggerProperty flyToFeature; + float boundingBoxDiagonal = 0.f; + }; + + void readFile(); + void parseSingleFeature(const geos::io::GeoJSONFeature& feature); + + /** + * Add meta properties to the feature, to allow things like flying to it, + * identifying its location, etc + */ + void addMetaPropertiesToFeature(SubFeatureProps& feature, int index, + const geos::geom::Geometry* geometry); + + void computeMainFeatureMetaPropeties(); + + /** + * Trigger a flight to a feature in the collection. No index means to fly to an + * overview of all features in the collection. + */ + void flyToFeature(std::optional index = std::nullopt) const; + + std::vector _geometryFeatures; + + properties::BoolProperty _enabled; + properties::StringProperty _geoJsonFile; + properties::FloatProperty _heightOffset; + properties::Vec2Property _latLongOffset; + + properties::FloatProperty _pointSizeScale; + properties::FloatProperty _lineWidthScale; + + GeoJsonProperties _defaultProperties; + + properties::OptionProperty _pointRenderModeOption; + + properties::BoolProperty _drawWireframe; + properties::BoolProperty _preventUpdatesFromHeightMap; + properties::TriggerProperty _forceUpdateHeightData; + + RenderableGlobe& _globeNode; + + bool _ignoreHeightsFromFile = false; + + bool _dataIsDirty = true; + bool _heightOffsetIsDirty = false; + bool _dataIsInitialized = false; + bool _textureIsDirty = false; + + properties::Vec2Property _centerLatLong; + float _bboxDiagonalSize = 0.f; + properties::TriggerProperty _flyToFeature; + + std::vector> _lightSources; + std::unique_ptr _defaultLightSource; + + rendering::helper::LightSourceRenderData _lightsourceRenderData; + + properties::PropertyOwner _lightSourcePropertyOwner; + properties::PropertyOwner _featuresPropertyOwner; + std::vector> _features; + + std::unique_ptr _linesAndPolygonsProgram = nullptr; + std::unique_ptr _pointsProgram = nullptr; +}; + +} // namespace openspace::globebrowsing + +#endif // __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONCOMPONENT___H__ diff --git a/modules/globebrowsing/src/geojson/geojsonmanager.cpp b/modules/globebrowsing/src/geojson/geojsonmanager.cpp new file mode 100644 index 0000000000..1b0850f4d2 --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsonmanager.cpp @@ -0,0 +1,146 @@ +/***************************************************************************************** + * * + * 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 + + +namespace { + constexpr std::string_view _loggerCat = "GeoJsonManager"; +} // namespace + +namespace openspace::globebrowsing { + +documentation::Documentation GeoJsonManager::Documentation() { + using namespace documentation; + return { + "GeoJsonManager", + "globebrowsing_geojsonmanager", + {} + }; +} + +// TODO: Gui name and description +GeoJsonManager::GeoJsonManager() : properties::PropertyOwner({ "GeoJson" }) {} + +void GeoJsonManager::initialize(RenderableGlobe* globe) { + ghoul_assert(globe, "No globe provided"); + _parentGlobe = globe; +} + +void GeoJsonManager::deinitializeGL() { + for (const std::unique_ptr& g : _geoJsonObjects) { + g->deinitializeGL(); + } +} + +bool GeoJsonManager::isReady() const { + bool isReady = std::all_of( + std::begin(_geoJsonObjects), + std::end(_geoJsonObjects), + [](const std::unique_ptr& g) { + return g->isReady(); + } + ); + return isReady; +} + +void GeoJsonManager::addGeoJsonLayer(const ghoul::Dictionary& layerDict) { + ZoneScoped + + try { + // Parse dictionary + documentation::testSpecificationAndThrow( + GeoJsonComponent::Documentation(), + layerDict, + "GeoJsonComponent" + ); + + std::string identifier = layerDict.value("Identifier"); + if (hasPropertySubOwner(identifier)) { + LERROR("GeoJson layer with identifier '" + identifier + "' already exists"); + return; + } + + // TODO: use owner instead of explicit parent? + std::unique_ptr geo = + std::make_unique(layerDict, *_parentGlobe); + + geo->initializeGL(); + + GeoJsonComponent* ptr = geo.get(); + _geoJsonObjects.push_back(std::move(geo)); + addPropertySubOwner(ptr); + } + catch (const documentation::SpecificationError& e) { + logError(e); + } + catch (const ghoul::RuntimeError& e) { + LERRORC(e.component, e.message); + } +} + +void GeoJsonManager::deleteLayer(const std::string& layerIdentifier) { + ZoneScoped + + for (auto it = _geoJsonObjects.begin(); it != _geoJsonObjects.end(); ++it) { + if (it->get()->identifier() == layerIdentifier) { + // we need to make a copy as the layerIdentifier is only a reference + // which will no longer be valid once it is deleted + std::string id = layerIdentifier; + removePropertySubOwner(it->get()); + (*it)->deinitializeGL(); + _geoJsonObjects.erase(it); + LINFO("Deleted GeoJson layer " + id); + return; + } + } + LERROR("Could not find GeoJson layer " + layerIdentifier); +} + +void GeoJsonManager::update() { + ZoneScoped + + for (std::unique_ptr& obj : _geoJsonObjects) { + if (obj->enabled()) { + obj->update(); + } + } +} + +void GeoJsonManager::render(const RenderData& data) { + ZoneScoped + + for (std::unique_ptr& obj : _geoJsonObjects) { + if (obj->enabled()) { + obj->render(data); + } + } +} + +} // namespace openspace::globebrowsing diff --git a/modules/globebrowsing/src/geojson/geojsonmanager.h b/modules/globebrowsing/src/geojson/geojsonmanager.h new file mode 100644 index 0000000000..7349f60875 --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsonmanager.h @@ -0,0 +1,74 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONMANAGER___H__ +#define __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONMANAGER___H__ + +#include + +#include + +#include +#include +#include + +namespace ghoul { class Dictionary; } +namespace openspace { struct RenderData; } +namespace openspace::documentation { struct Documentation; } + +namespace openspace::globebrowsing { + +class RenderableGlobe; + +/** + * Manages multiple GeoJSON objects of a globe + */ +class GeoJsonManager : public properties::PropertyOwner { +public: + GeoJsonManager(); + + void initialize(RenderableGlobe* globe); + void deinitializeGL(); + + bool isReady() const; + + void addGeoJsonLayer(const ghoul::Dictionary& layerDict); + //void addGeoJsonLayer(const std::string& filePath); // TODO: just add from file + void deleteLayer(const std::string& layerIdentifier); + + void update(); + void render(const RenderData& data); + + //void onChange(std::function callback); + + static documentation::Documentation Documentation(); + +private: + std::vector> _geoJsonObjects; + RenderableGlobe* _parentGlobe = nullptr; +}; + +} // namespace openspace::globebrowsing + +#endif // __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONMANAGER___H__ diff --git a/modules/globebrowsing/src/geojson/geojsonproperties.cpp b/modules/globebrowsing/src/geojson/geojsonproperties.cpp new file mode 100644 index 0000000000..871136d132 --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsonproperties.cpp @@ -0,0 +1,617 @@ +/***************************************************************************************** + * * + * 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 + +// Keys used to read properties from GeoJson files +namespace geojson::propertykeys { + constexpr std::string_view Name = "name"; + constexpr std::string_view Description = "description"; + + constexpr std::string_view Opacity = "opacity"; + + constexpr std::array Color = { "color", "stroke" }; + constexpr std::array FillColor = { "fill", "fill-color" }; + constexpr std::string_view FillOpacity = "fill-opacity"; + constexpr std::string_view LineWidth = "stroke-width"; + + constexpr std::string_view PointSize = "point-size"; + constexpr std::array Texture = { "texture", "sprite", "point-texture" }; + + constexpr std::array PointTextureAnchor = { "point-anchor", "anchor" }; + constexpr std::string_view PointTextureAnchorBottom = "bottom"; + constexpr std::string_view PointTextureAnchorCenter = "center"; + + constexpr std::string_view Extrude = "extrude"; + + constexpr std::string_view PerformShading = "performShading"; + + constexpr std::string_view Tessellate = "tessellate"; + constexpr std::string_view TessellationLevel = "tessellationLevel"; + constexpr std::string_view TessellationMaxDistance = "tessellationDistance"; + + constexpr std::string_view AltitudeMode = "altitudeMode"; + constexpr std::string_view AltitudeModeClamp = "clampToGround"; + constexpr std::string_view AltitudeModeAbsolute = "absolute"; + constexpr std::string_view AltitudeModeRelative = "relativeToGround"; +} // namespace geojson::propertykeys + +namespace { + using PropertyInfo = openspace::properties::Property::PropertyInfo; + + template + bool keyMatches(const std::string_view key, + const std::array& keyAlternativesArray, + std::optional propInfo = std::nullopt) + { + auto it = std::find(keyAlternativesArray.begin(), keyAlternativesArray.end(), key); + if (it != keyAlternativesArray.end()) { + return true; + } + + if (propInfo.has_value() && key == (*propInfo).identifier) { + return true; + } + + return false; + } + + bool keyMatches(const std::string_view key, const std::string_view keyAlternative, + std::optional propInfo = std::nullopt) + { + const std::array array = { keyAlternative }; + return keyMatches(key, array, propInfo); + } + + glm::vec3 hexToRbg(std::string_view hexColor) { + int r, g, b; + // @TODO: Consider using scn::scan instead of sscanf + int ret = std::sscanf(hexColor.data(), "#%02x%02x%02x", &r, &g, &b); + // @TODO: Handle return value to validate color + return (1.f / 255.f) * glm::vec3(r, g, b); + } + + glm::vec3 getColorValue(const geos::io::GeoJSONValue& value) { + glm::vec3 color; + if (value.isArray()) { + const std::vector& val = value.getArray(); + if (val.size() != 3) { + // @TODO: Should add some more information on which file the reading failed for + LERRORC("GeoJson", fmt::format( + "Failed reading color property. Expected 3 values, got {}", val.size() + )); + } + // @TODO Use verifiers to verify color values + // @TODO Parse values given in RGB in ranges 0-255? + color = glm::vec3( + static_cast(val.at(0).getNumber()), + static_cast(val.at(1).getNumber()), + static_cast(val.at(2).getNumber()) + ); + } + else if (value.isString()) { + const std::string hex = value.getString(); + // @TODO Verify color + color = hexToRbg(hex); + } + return color; + }; + + constexpr openspace::properties::Property::PropertyInfo OpacityInfo = { + "Opacity", + "Opacity", + "This value determines the opacity of this object. A value of 0 means " + "completely transparent", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo ColorInfo = { + "Color", + "Color", + "The color of the rendered geometry. For points it will be used as a multiply" + "color for any provided texture", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo FillOpacityInfo = { + "FillOpacity", + "Fill Opacity", + "This value determines the opacity of the filled portion of a polygon. Will " + "also be used for extruded features", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo FillColorInfo = { + "FillColor", + "Fill Color", + "The color of the filled portion of a rendered polygon. Will also be used for " + "extruded features", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo LineWidthInfo = { + "LineWidth", + "Line Width", + "The width of any rendered lines", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo PointSizeInfo = { + "PointSize", + "Point Size", + "The size of any rendered points. The size will be scaled based on the " + "bounding sphere of the globe", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo PointTextureInfo = { + "PointTexture", + "Point Texture", + "A texture to be used for rendering points. No value means to use the default " + "texture provided by the GlobeBrowsing module. If no texture is provided there " + "either, the point will be rendered as a plane and colored by the color value.", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo ExtrudeInfo = { + "Extrude", + "Extrude", + "If true, extrude the mesh or line to intersect the globe", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo PerformShadingInfo = { + "PerformShading", + "Perform Shading", + "If true, perform shading on any create meshes, either from polygons or " + "extruded lines. The shading will be computed based on any light sources of the " + "GeoJson component", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo AltitudeModeInfo = { + "AltitudeMode", + "Altitude Mode", + "The altitude mode decides how any height values of the geo coordinates should " + "be interpreted. Absolute means that the height is interpreted as the height " + "above the reference ellipsoid, while RelativeToGround takes the height map " + "into account. For coordinates with only two values (latitude and longitude), " + "the height is considered to be equal to zero", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo PointAnchorOptionInfo = { + "PointTextureAnchor", + "Point Texture Anchor", + "Decides the placement of the point texture in relation to the position. " + "Default is a the bottom of the texture, but it can also be put at the center", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo TessellationEnabledInfo = { + "Enabled", + "Enabled", + "If false, no tessellation to bend the geometry based on the curvature of the " + "planet is performed. This leads to increased performance, but tessellation is " + "neccessary for large geometry that spans a big portion of the globe. Otherwise " + "it may intersect the surface", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo UseTessellationLevelInfo = { + "UseTessellationLevel", + "Use Tessellation Level", + "If true, use the 'Tesselation Level' to control the level of detail for the " + "tessellation. The distance used will be the 'Tesselation Distance' divided by " + "the 'Tesselation Level', so the higher the level value, the smaller each " + "segment in the geomoetry will be", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo TessellationLevelInfo = { + "TessellationLevel", + "Tessellation Level", + "When manual tessellation is enabled, this value will be used to determine how " + "much tessellation to apply. The resulting distance used for subdividing the " + "geometry will be the 'Tessellation Distance' divided by this value. Zero means " + "to use the 'Tessellation Distance' as is.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo TessellationDistanceInfo = { + "TessellationDistance", + "Tessellation Distance", + "Defult distance to use for tessellation of line and polygon geometry. Anything " + "larger than this distance will be automatically subdivided into smaller pieces " + "matching this distance, while anything smaller will not be subdivided. Per " + "default this will be set to a distance corresponding to about 1 degree " + "longitude on the globe", + openspace::properties::Property::Visibility::AdvancedUser + }; + + struct [[codegen::Dictionary(GeoJsonProperties)]] Parameters { + // [[codegen::verbatim(OpacityInfo.description)]] + std::optional opacity [[codegen::inrange(0.0, 1.0)]]; + + // [[codegen::verbatim(ColorInfo.description)]] + std::optional color [[codegen::color()]]; + + // [[codegen::verbatim(FillOpacityInfo.description)]] + std::optional fillOpacity [[codegen::inrange(0.0, 1.0)]]; + + // [[codegen::verbatim(FillColorInfo.description)]] + std::optional fillColor [[codegen::color()]]; + + // [[codegen::verbatim(LineWidthInfo.description)]] + std::optional lineWidth [[codegen::greater(0.0)]]; + + // [[codegen::verbatim(PointSizeInfo.description)]] + std::optional pointSize [[codegen::greater(0.0)]]; + + // [[codegen::verbatim(PointTextureInfo.description)]] + std::optional pointTexture; + + // [[codegen::verbatim(ExtrudeInfo.description)]] + std::optional extrude; + + // [[codegen::verbatim(PerformShadingInfo.description)]] + std::optional performShading; + + enum class [[codegen::map(openspace::globebrowsing::GeoJsonProperties::AltitudeMode)]] AltitudeMode { + Absolute, + RelativeToGround + }; + // [[codegen::verbatim(AltitudeModeInfo.description)]] + std::optional altitudeMode; + + enum class [[codegen::map(openspace::globebrowsing::GeoJsonProperties::PointTextureAnchor)]] PointTextureAnchor { + Bottom, + Center + }; + // [[codegen::verbatim(PointAnchorOptionInfo.description)]] + std::optional pointTextureAnchor; + + struct Tessellation { + // [[codegen::verbatim(TessellationEnabledInfo.description)]] + std::optional ebabled; + + // [[codegen::verbatim(UseTessellationLevelInfo.description)]] + std::optional useTessellationLevel; + + // [[codegen::verbatim(TessellationLevelInfo.description)]] + std::optional tessellationLevel; + + // [[codegen::verbatim(TessellationDistanceInfo.description)]] + std::optional tessellationDistance; + }; + std::optional tessellation; + }; +#include "geojsonproperties_codegen.cpp" +} // namespace + +namespace openspace::globebrowsing { + +documentation::Documentation GeoJsonProperties::Documentation() { + return codegen::doc("globebrowsing_geojsonproperties"); +} + +GeoJsonProperties::Tessellation::Tessellation() + : properties::PropertyOwner({ "Tessellation" }) + , enabled(TessellationEnabledInfo, true) + , useLevel(UseTessellationLevelInfo, false) + , level(TessellationLevelInfo, 10, 0, 100) + , distance(TessellationDistanceInfo, 100000.f, 0.f, 1e12f) +{ + addProperty(enabled); + addProperty(distance); + addProperty(level); + addProperty(useLevel); +} + +GeoJsonProperties::GeoJsonProperties() + : properties::PropertyOwner({ "DefaultProperties" }) + , opacity(OpacityInfo, 1.f, 0.f, 1.f) + , color(ColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f)) + , fillOpacity(FillOpacityInfo, 0.7f, 0.f, 1.f) + , fillColor(FillColorInfo, glm::vec3(0.5f), glm::vec3(0.f), glm::vec3(1.f)) + , lineWidth(LineWidthInfo, 2.f, 0.01f, 10.f) + , pointSize(PointSizeInfo, 10.f, 0.01f, 100.f) + , pointTexture(PointTextureInfo) + , extrude(ExtrudeInfo, false) + , performShading(PerformShadingInfo, false) + , altitudeModeOption( + AltitudeModeInfo, + properties::OptionProperty::DisplayType::Dropdown + ) + , pointAnchorOption( + PointAnchorOptionInfo, + properties::OptionProperty::DisplayType::Dropdown + ) +{ + addProperty(opacity); + color.setViewOption(properties::Property::ViewOptions::Color); + addProperty(color); + addProperty(fillOpacity); + fillColor.setViewOption(properties::Property::ViewOptions::Color); + addProperty(fillColor); + + addProperty(lineWidth); + + addProperty(pointSize); + addProperty(pointTexture); + + addProperty(extrude); + addProperty(performShading); + + altitudeModeOption.addOptions({ + { static_cast(AltitudeMode::Absolute), "Absolute"}, + { static_cast(AltitudeMode::RelativeToGround), "Relative to Ground" } + //{ static_cast(AltitudeMode::ClampToGround), "Clamp to Ground" } / TODO: add ClampToGround + }); + addProperty(altitudeModeOption); + + pointAnchorOption.addOptions({ + { static_cast(PointTextureAnchor::Bottom), "Bottom"}, + { static_cast(PointTextureAnchor::Center), "Center" } + }); + addProperty(pointAnchorOption); + + addPropertySubOwner(tessellation); +} + +void GeoJsonProperties::createFromDictionary(const ghoul::Dictionary& dictionary, + const RenderableGlobe& globe) +{ + const Parameters p = codegen::bake(dictionary); + opacity = p.opacity.value_or(opacity); + color = p.color.value_or(color); + + fillOpacity = p.fillOpacity.value_or(fillOpacity); + fillColor = p.fillColor.value_or(fillColor); + + lineWidth = p.lineWidth.value_or(lineWidth); + + pointSize = p.pointSize.value_or(pointSize); + + if (p.pointTexture.has_value()) { + pointTexture = *p.pointTexture; + } + + if (p.pointTextureAnchor.has_value()) { + pointAnchorOption = static_cast(codegen::map( + *p.pointTextureAnchor + )); + } + + extrude = p.extrude.value_or(extrude); + performShading = p.performShading.value_or(performShading); + + if (p.altitudeMode.has_value()) { + altitudeModeOption = static_cast(codegen::map(*p.altitudeMode)); + } + + // Set up default value and max value for tessellation distance based on globe size. + // Distances are computed based on a certain lat/long angle size + constexpr float DefaultAngle = glm::radians(1.f); + constexpr float MaxAngle = glm::radians(45.f); + float defaultDistance = globe.ellipsoid().longitudalDistance(0.f, 0.f, DefaultAngle); + float maxDistance = globe.ellipsoid().longitudalDistance(0.f, 0.f, MaxAngle); + tessellation.distance = defaultDistance; + tessellation.distance.setMaxValue(maxDistance); + + if (p.tessellation.has_value()) { + const Parameters::Tessellation pTess = (*p.tessellation); + tessellation.enabled = pTess.useTessellationLevel.value_or(tessellation.enabled); + tessellation.useLevel = + pTess.useTessellationLevel.value_or(tessellation.useLevel); + tessellation.level = pTess.tessellationLevel.value_or(tessellation.level); + tessellation.distance = + pTess.tessellationDistance.value_or(tessellation.distance); + } +} + +GeoJsonProperties::AltitudeMode GeoJsonProperties::altitudeMode() const { + return static_cast(altitudeModeOption.value()); +} + +GeoJsonProperties::PointTextureAnchor GeoJsonProperties::pointTextureAnchor() const { + return static_cast(pointAnchorOption.value()); +} + +GeoJsonOverrideProperties propsFromGeoJson(const geos::io::GeoJSONFeature& feature) { + const std::map& props = feature.getProperties(); + GeoJsonOverrideProperties result; + + auto parseProperty = [&result](const std::string_view key, + const geos::io::GeoJSONValue& value) + { + using namespace geojson; + + if (keyMatches(key, propertykeys::Name)) { + result.name = value.getString(); + } + else if (keyMatches(key, propertykeys::Opacity, OpacityInfo)) { + result.opacity = static_cast(value.getNumber()); + } + else if (keyMatches(key, propertykeys::Color, ColorInfo)) { + result.color = getColorValue(value); + } + else if (keyMatches(key, propertykeys::FillOpacity, FillOpacityInfo)) { + result.fillOpacity = static_cast(value.getNumber()); + } + else if (keyMatches(key, propertykeys::FillColor, FillColorInfo)) { + result.fillColor = getColorValue(value); + } + else if (keyMatches(key, propertykeys::LineWidth, LineWidthInfo)) { + result.lineWidth = static_cast(value.getNumber()); + } + else if (keyMatches(key, propertykeys::PointSize, PointSizeInfo)) { + result.pointSize = static_cast(value.getNumber()); + } + else if (keyMatches(key, propertykeys::Texture, PointTextureInfo)) { + result.pointTexture = value.getString(); + } + else if (keyMatches(key, propertykeys::PointTextureAnchor, PointAnchorOptionInfo)) + { + std::string mode = value.getString(); + + if (mode == propertykeys::PointTextureAnchorBottom) { + result.pointTextureAnchor = GeoJsonProperties::PointTextureAnchor::Bottom; + } + else if (mode == propertykeys::PointTextureAnchorCenter) { + result.pointTextureAnchor = GeoJsonProperties::PointTextureAnchor::Center; + } + else { + LERRORC("GeoJson", fmt::format( + "Point texture anchor mode '{}' not supported", mode + )); + } + } + else if (keyMatches(key, propertykeys::Extrude, ExtrudeInfo)) { + result.extrude = value.getBoolean(); + } + else if (keyMatches(key, propertykeys::AltitudeMode, AltitudeModeInfo)) { + std::string mode = value.getString(); + + if (mode == propertykeys::AltitudeModeAbsolute) { + result.altitudeMode = GeoJsonProperties::AltitudeMode::Absolute; + } + else if (mode == propertykeys::AltitudeModeRelative) { + result.altitudeMode = GeoJsonProperties::AltitudeMode::RelativeToGround; + } + // @TODO Include when support is implemented + //else if (mode == propertykeys::AltitudeModeClamp) { + // result.altitudeMode = GeoJsonProperties::AltitudeMode::ClampToGround; + //} + else { + LERRORC("GeoJson", fmt::format( + "Altitude mode '{}' not supported", mode + )); + } + } + else if (keyMatches(key, propertykeys::Tessellate, TessellationEnabledInfo)) { + result.tessellationEnabled = value.getBoolean(); + } + else if (keyMatches(key, propertykeys::TessellationLevel, TessellationLevelInfo)) { + result.useTessellationLevel = true; + result.tessellationLevel = static_cast(value.getNumber()); + } + else if (keyMatches(key, propertykeys::TessellationMaxDistance, TessellationDistanceInfo)) { + result.tessellationDistance = static_cast(value.getNumber()); + } + }; + + for (auto const& [key, value] : props) { + try { + parseProperty(key, value); + } + catch (const geos::io::GeoJSONValue::GeoJSONTypeError&) { + // @TODO: Should add some more information on which file the reading failed for + LERRORC("GeoJson", fmt::format( + "Error reading GeoJson property '{}'. Value has wrong type", key + )); + } + } + + return result; +} + +float PropertySet::opacity() const { + return overrideValues.opacity.value_or(defaultValues.opacity); +} + +glm::vec3 PropertySet::color() const { + return overrideValues.color.value_or(defaultValues.color); +} + +float PropertySet::fillOpacity() const { + return overrideValues.fillOpacity.value_or(defaultValues.fillOpacity); +} + +glm::vec3 PropertySet::fillColor() const { + return overrideValues.fillColor.value_or(defaultValues.fillColor); +} + +float PropertySet::lineWidth() const { + return overrideValues.lineWidth.value_or(defaultValues.lineWidth); +} + +float PropertySet::pointSize() const { + return overrideValues.pointSize.value_or(defaultValues.pointSize); +} + +std::string PropertySet::pointTexture() const { + return overrideValues.pointTexture.value_or(defaultValues.pointTexture); +} + +GeoJsonProperties::PointTextureAnchor PropertySet::pointTextureAnchor() const { + return overrideValues.pointTextureAnchor.value_or( + defaultValues.pointTextureAnchor() + ); +} + +bool PropertySet::extrude() const { + return overrideValues.extrude.value_or(defaultValues.extrude); +} + +bool PropertySet::performShading() const { + return overrideValues.performShading.value_or(defaultValues.performShading); +} + +GeoJsonProperties::AltitudeMode PropertySet::altitudeMode() const { + return overrideValues.altitudeMode.value_or(defaultValues.altitudeMode()); +} + +bool PropertySet::tessellationEnabled() const { + return overrideValues.tessellationEnabled.value_or(defaultValues.tessellation.enabled); +} + +bool PropertySet::useTessellationLevel() const { + return overrideValues.useTessellationLevel.value_or( + defaultValues.tessellation.useLevel + ); +} + +int PropertySet::tessellationLevel() const { + return overrideValues.tessellationLevel.value_or(defaultValues.tessellation.level); +} + +float PropertySet::tessellationDistance() const { + return overrideValues.tessellationDistance.value_or( + defaultValues.tessellation.distance + ); +} + +bool PropertySet::hasOverrideTexture() const { + return overrideValues.pointTexture.has_value(); +} + +} // namespace openspace::globebrowsing diff --git a/modules/globebrowsing/src/geojson/geojsonproperties.h b/modules/globebrowsing/src/geojson/geojsonproperties.h new file mode 100644 index 0000000000..de5bab77ee --- /dev/null +++ b/modules/globebrowsing/src/geojson/geojsonproperties.h @@ -0,0 +1,151 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONPROPERTIES___H__ +#define __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONPROPERTIES___H__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace geos::io { class GeoJSONFeature; } + +namespace openspace::globebrowsing { + +class RenderableGlobe; + +struct GeoJsonProperties : public properties::PropertyOwner { + GeoJsonProperties(); + void createFromDictionary(const ghoul::Dictionary& dictionary, + const RenderableGlobe& globe); + + static documentation::Documentation Documentation(); + + // These are based on the KML specification + enum class AltitudeMode { + // Compute position as an altitude above the reference ellipsoid + Absolute = 0, + // Compute position using altitude above the height map + RelativeToGround + // Stick to planet surface (TODO: use GDAL to render layer instead and use as default) + //ClampToGround + }; + + enum class PointTextureAnchor { + Bottom = 0, + Center + }; + + struct Tessellation : public properties::PropertyOwner { + Tessellation(); + properties::BoolProperty enabled; + properties::BoolProperty useLevel; + properties::IntProperty level; + properties::FloatProperty distance; + } tessellation; + + AltitudeMode altitudeMode() const; + PointTextureAnchor pointTextureAnchor() const; + + properties::FloatProperty opacity; + properties::Vec3Property color; + properties::FloatProperty fillOpacity; + properties::Vec3Property fillColor; + properties::FloatProperty lineWidth; + + properties::FloatProperty pointSize; + properties::StringProperty pointTexture; + properties::OptionProperty pointAnchorOption; + + properties::BoolProperty extrude; + properties::BoolProperty performShading; + properties::OptionProperty altitudeModeOption; +}; + +// Optional versions of all the properties above, that can be read from a geoJson file +// and used to override any default values +struct GeoJsonOverrideProperties { + std::optional name; + + std::optional opacity; + std::optional color; + std::optional fillOpacity; + std::optional fillColor; + std::optional lineWidth; + + std::optional pointSize; + std::optional pointTexture; + std::optional pointTextureAnchor; + + std::optional extrude; + std::optional performShading; + std::optional altitudeMode; + + std::optional tessellationEnabled; + std::optional useTessellationLevel; + std::optional tessellationLevel; + std::optional tessellationDistance; +}; + +GeoJsonOverrideProperties propsFromGeoJson(const geos::io::GeoJSONFeature& feature); + +struct PropertySet { + // This value set should be a reference to the main component's propertyowner + GeoJsonProperties& defaultValues; + // This is a unique set of properties to use for overriding the default values + GeoJsonOverrideProperties overrideValues; + + float opacity() const; + glm::vec3 color() const; + float fillOpacity() const; + glm::vec3 fillColor() const; + float lineWidth() const; + + float pointSize() const; + std::string pointTexture() const; + GeoJsonProperties::PointTextureAnchor pointTextureAnchor() const; + + bool extrude() const; + bool performShading() const; + GeoJsonProperties::AltitudeMode altitudeMode() const; + + bool tessellationEnabled() const; + bool useTessellationLevel() const; + int tessellationLevel() const; + float tessellationDistance() const; + + bool hasOverrideTexture() const; +}; + +} // namespace openspace::globebrowsing + +#endif // __OPENSPACE_MODULE_GLOBEBROWSING___GEOJSONPROPERTIES___H__ diff --git a/modules/globebrowsing/src/geojson/globegeometryfeature.cpp b/modules/globebrowsing/src/geojson/globegeometryfeature.cpp new file mode 100644 index 0000000000..d771fc63e8 --- /dev/null +++ b/modules/globebrowsing/src/geojson/globegeometryfeature.cpp @@ -0,0 +1,914 @@ +/***************************************************************************************** + * * + * 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 + +namespace { + constexpr const char* _loggerCat = "GlobeGeometryFeature"; + + constexpr std::chrono::milliseconds HeightUpdateInterval(10000); +} // namespace + +namespace openspace::globebrowsing { + +void GlobeGeometryFeature::RenderFeature::initializeBuffers() { + if (vaoId == 0) { + glGenVertexArrays(1, &vaoId); + } + if (vboId == 0) { + glGenBuffers(1, &vboId); + } +} + +GlobeGeometryFeature::GlobeGeometryFeature(const RenderableGlobe& globe, + GeoJsonProperties& defaultProperties, + GeoJsonOverrideProperties& overrideProperties) + : _globe(globe) + , _properties({ defaultProperties, overrideProperties }) + , _lastHeightUpdateTime(std::chrono::system_clock::now()) +{} + +std::string GlobeGeometryFeature::key() const { + return _key; +} + +void GlobeGeometryFeature::setOffsets(const glm::vec3& value) { + _offsets = value; +} + +void GlobeGeometryFeature::initializeGL(ghoul::opengl::ProgramObject* pointsProgram, + ghoul::opengl::ProgramObject* linesAndPolygonsProgram) +{ + _pointsProgram = pointsProgram; + _linesAndPolygonsProgram = linesAndPolygonsProgram; + + if (isPoints()) { + updateTexture(true); + } +} + +void GlobeGeometryFeature::deinitializeGL() { + for (const RenderFeature& r : _renderFeatures) { + glDeleteVertexArrays(1, &r.vaoId); + glDeleteBuffers(1, &r.vboId); + } + + _pointTexture = nullptr; +} + +bool GlobeGeometryFeature::isReady() const { + bool shadersAreReady = _linesAndPolygonsProgram && _pointsProgram; + bool textureIsReady = (!_hasTexture) || _pointTexture; + return shadersAreReady && textureIsReady; +} + +bool GlobeGeometryFeature::isPoints() const { + return _type == GeometryType::Point; +} + +bool GlobeGeometryFeature::useHeightMap() const { + return _properties.altitudeMode() == + GeoJsonProperties::AltitudeMode::RelativeToGround; +} + +void GlobeGeometryFeature::updateTexture(bool isInitializeStep) { + std::string texture; + GlobeBrowsingModule* m = global::moduleEngine->module(); + + if (!isInitializeStep && _properties.hasOverrideTexture()) { + // Here we don't necessarily have to update, since it should have been + // created at initialization. Do nothing + return; + } + else if (!_properties.pointTexture().empty()) { + texture = _properties.pointTexture(); + } + else if (m->hasDefaultGeoPointTexture()) { + texture = m->defaultGeoPointTexture(); + } + else { + // No texture => render without texture + _hasTexture = false; + _pointTexture = nullptr; + return; + } + + if (isInitializeStep || !_pointTexture) { + _pointTexture = std::make_unique(2); + _pointTexture->setFilterMode(ghoul::opengl::Texture::FilterMode::AnisotropicMipMap); + _pointTexture->setWrapping(ghoul::opengl::Texture::WrappingMode::ClampToEdge); + } + + std::filesystem::path texturePath = absPath(texture); + if (std::filesystem::is_regular_file(texturePath)) { + _hasTexture = true; + _pointTexture->loadFromFile(texture); + _pointTexture->uploadToGpu(); + } + else { + LERROR(fmt::format( + "Trying to use texture file that does not exist: {} ", texturePath + )); + } +} + +void GlobeGeometryFeature::createFromSingleGeosGeometry(const geos::geom::Geometry* geo, + int index, bool ignoreHeights) +{ + ghoul_assert(geo, "No geometry provided"); + ghoul_assert( + geo->isPuntal() || !geo->isCollection(), + "Non-point geometry can not be a collection" + ); + + switch (geo->getGeometryTypeId()) { + case geos::geom::GEOS_POINT: + case geos::geom::GEOS_MULTIPOINT: { + _geoCoordinates.push_back(geometryhelper::geometryCoordsAsGeoVector(geo)); + _type = GeometryType::Point; + break; + } + case geos::geom::GEOS_LINESTRING: { + _geoCoordinates.push_back(geometryhelper::geometryCoordsAsGeoVector(geo)); + _type = GeometryType::LineString; + break; + } + case geos::geom::GEOS_POLYGON: { + try { + const geos::geom::Polygon* p = dynamic_cast(geo); + + // Triangles + // Note that Constrained Delaunay triangulation supports polygons with holes :) + std::vector triCoords; + TriList triangles; + using geos::triangulate::polygon::ConstrainedDelaunayTriangulator; + ConstrainedDelaunayTriangulator::triangulatePolygon(p, triangles); + + triCoords.reserve(3 * triangles.size()); + + // Add three coordinates per triangle. Note flipped winding order + // (want counter clockwise, but GEOS provides clockwise) + for (const Tri* t : triangles) { + triCoords.push_back(t->getCoordinate(0)); + triCoords.push_back(t->getCoordinate(2)); + triCoords.push_back(t->getCoordinate(1)); + } + _triangleCoordinates = geometryhelper::coordsToGeodetic(triCoords); + + // Boundaries / Lines + + // Normalize to make sure rings have correct orientation + std::unique_ptr pNormalized = p->clone(); + pNormalized->normalize(); + + const geos::geom::LinearRing* outerRing = pNormalized->getExteriorRing(); + std::vector outerBoundsGeoCoords = + geometryhelper::geometryCoordsAsGeoVector(outerRing); + + if (!outerBoundsGeoCoords.empty()) { + int nHoles = static_cast(pNormalized->getNumInteriorRing()); + _geoCoordinates.reserve(nHoles + 1); + + // Outer bounds + _geoCoordinates.push_back(outerBoundsGeoCoords); + + // Inner bounds (holes) + for (int i = 0; i < nHoles; ++i) { + const geos::geom::LinearRing* hole = pNormalized->getInteriorRingN(i); + std::vector ringGeoCoords = + geometryhelper::geometryCoordsAsGeoVector(hole); + + _geoCoordinates.push_back(ringGeoCoords); + } + } + + _type = GeometryType::Polygon; + } + catch (geos::util::IllegalStateException&) { + LERROR("Non-simple (e.g. self-intersecting) polygons not supported yet"); + throw ghoul::MissingCaseException(); + + // TODO: handle self-intersections points + // https://www.sciencedirect.com/science/article/pii/S0304397520304199 + } + break; + } + default: + throw ghoul::MissingCaseException(); + } + + // Reset height values if we don't care about them + if (ignoreHeights) { + for (std::vector& vec : _geoCoordinates) { + for (Geodetic3& coord : vec) { + coord.height = 0.0; + } + } + } + + // Compute reference positions to use for checking if height map changes + geos::geom::Coordinate centroid; + geo->getCentroid(centroid); + Geodetic3 geoCentroid = geometryhelper::coordsToGeodetic({ centroid }).front(); + _heightUpdateReferencePoints.push_back(std::move(geoCentroid)); +; + std::vector envelopeGeoCoords = + geometryhelper::geometryCoordsAsGeoVector(geo->getEnvelope().get()); + + _heightUpdateReferencePoints.insert( + _heightUpdateReferencePoints.end(), + envelopeGeoCoords.begin(), + envelopeGeoCoords.end() + ); + + if (_properties.overrideValues.name.has_value()) { + _key = *_properties.overrideValues.name; + } + else { + _key = fmt::format("Feature {} - {}", index, geo->getGeometryType()); + } +} + +void GlobeGeometryFeature::render(const RenderData& renderData, int pass, + float mainOpacity, + const ExtraRenderData& extraRenderData) +{ + ghoul_assert(pass >= 0 && pass < 2, "Render pass variable out of accepted range"); + + float opacity = mainOpacity * _properties.opacity(); + float fillOpacity = mainOpacity * _properties.fillOpacity(); + + const glm::dmat4 globeModelTransform = _globe.modelTransform(); + const glm::dmat4 modelViewTransform = + renderData.camera.combinedViewMatrix() * globeModelTransform; + + const glm::mat3 normalTransform = glm::mat3( + glm::transpose(glm::inverse(modelViewTransform)) + ); + + const glm::dmat4 projectionTransform = renderData.camera.projectionMatrix(); + +#ifndef __APPLE__ + glLineWidth(_properties.lineWidth() * extraRenderData.lineWidthScale); +#else + glLineWidth(1.f); +#endif + + for (const RenderFeature& r : _renderFeatures) { + if (r.isExtrusionFeature && !_properties.extrude()) { + continue; + } + + bool shouldRenderTwice = r.type == RenderType::Polygon && + fillOpacity < 1.f && _properties.extrude(); + + if (pass > 0 && !shouldRenderTwice) { + continue; + } + + ghoul::opengl::ProgramObject* shader = (r.type == RenderType::Points) ? + _pointsProgram : _linesAndPolygonsProgram; + + shader->activate(); + shader->setUniform("modelTransform", globeModelTransform); + shader->setUniform("viewTransform", renderData.camera.combinedViewMatrix()); + shader->setUniform("projectionTransform", projectionTransform); + + shader->setUniform("heightOffset", _offsets.z); + shader->setUniform("useHeightMapData", useHeightMap()); + + if (shader == _linesAndPolygonsProgram) { + const rendering::helper::LightSourceRenderData& ls = + extraRenderData.lightSourceData; + shader->setUniform("normalTransform", normalTransform); + shader->setUniform("nLightSources", ls.nLightSources); + shader->setUniform("lightIntensities", ls.intensitiesBuffer); + shader->setUniform("lightDirectionsViewSpace", ls.directionsViewSpaceBuffer); + } + + glBindVertexArray(r.vaoId); + + switch (r.type) { + case RenderType::Lines: + shader->setUniform( + "opacity", + r.isExtrusionFeature ? fillOpacity : opacity + ); + renderLines(r); + break; + case RenderType::Points: { + shader->setUniform("opacity", opacity); + float scale = extraRenderData.pointSizeScale; + renderPoints(r, renderData, extraRenderData.pointRenderMode, scale); + break; + } + case RenderType::Polygon: { + shader->setUniform("opacity", fillOpacity); + renderPolygons(r, shouldRenderTwice, pass); + break; + } + default: + throw ghoul::MissingCaseException(); + break; + } + + shader->deactivate(); + } + + glBindVertexArray(0); + + // Reset when we're done rendering all the polygon features + global::renderEngine->openglStateCache().resetPolygonAndClippingState(); +} + +void GlobeGeometryFeature::renderPoints(const RenderFeature& feature, + const RenderData& renderData, + const PointRenderMode& renderMode, + float sizeScale) const +{ + ghoul_assert(feature.type == RenderType::Points, "Trying to render faulty geometry"); + _pointsProgram->setUniform("color", _properties.color()); + + float bs = static_cast(_globe.boundingSphere()); + float size = 0.001f * sizeScale * _properties.pointSize() * bs; + _pointsProgram->setUniform("pointSize", size); + + _pointsProgram->setUniform("renderMode", static_cast(renderMode)); + + using TextureAnchor = GeoJsonProperties::PointTextureAnchor; + _pointsProgram->setUniform( + "useBottomAnchorPoint", + _properties.pointTextureAnchor() != TextureAnchor::Center + ); + + // Points are rendered as billboards + glm::dvec3 cameraViewDirectionWorld = -renderData.camera.viewDirectionWorldSpace(); + glm::dvec3 cameraUpDirectionWorld = renderData.camera.lookUpVectorWorldSpace(); + glm::dvec3 orthoRight = glm::normalize( + glm::cross(cameraUpDirectionWorld, cameraViewDirectionWorld) + ); + if (orthoRight == glm::dvec3(0.0)) { + // For some reason, the up vector and camera view vecter were the same. Use a + // slightly different vector + glm::dvec3 otherVector = glm::vec3( + cameraUpDirectionWorld.y, + cameraUpDirectionWorld.x, + cameraUpDirectionWorld.z + ); + orthoRight = glm::normalize(glm::cross(otherVector, cameraViewDirectionWorld)); + } + glm::dvec3 orthoUp = glm::normalize(glm::cross(cameraViewDirectionWorld, orthoRight)); + + _pointsProgram->setUniform("cameraUp", glm::vec3(orthoUp)); + _pointsProgram->setUniform("cameraRight", glm::vec3(orthoRight)); + + glm::dvec3 cameraPositionWorld = renderData.camera.positionVec3(); + _pointsProgram->setUniform("cameraPosition", cameraPositionWorld); + _pointsProgram->setUniform("cameraLookUp", glm::vec3(cameraUpDirectionWorld)); + + if (_pointTexture && _hasTexture) { + ghoul::opengl::TextureUnit unit; + unit.activate(); + _pointTexture->bind(); + _pointsProgram->setUniform("pointTexture", unit); + _pointsProgram->setUniform("hasTexture", true); + + float widthHeightRatio = static_cast(_pointTexture->texture()->width()) / + static_cast(_pointTexture->texture()->height()); + _pointsProgram->setUniform("textureWidthFactor", widthHeightRatio); + } + else { + glBindTexture(GL_TEXTURE_2D, 0); + _pointsProgram->setUniform("hasTexture", false); + } + + glEnable(GL_PROGRAM_POINT_SIZE); + glDrawArrays(GL_POINTS, 0, static_cast(feature.nVertices)); + glDisable(GL_PROGRAM_POINT_SIZE); +} + +void GlobeGeometryFeature::renderLines(const RenderFeature& feature) const { + ghoul_assert(feature.type == RenderType::Lines, "Trying to render faulty geometry"); + + const glm::vec3 color = feature.isExtrusionFeature ? + _properties.fillColor() : _properties.color(); + + _linesAndPolygonsProgram->setUniform("color", color); + _linesAndPolygonsProgram->setUniform("performShading", false); + + glEnable(GL_LINE_SMOOTH); + glDrawArrays(GL_LINE_STRIP, 0, static_cast(feature.nVertices)); + glDisable(GL_LINE_SMOOTH); +} + +void GlobeGeometryFeature::renderPolygons(const RenderFeature& feature, + bool shouldRenderTwice, int renderPass) const +{ + ghoul_assert(renderPass == 0 || renderPass == 1, "Invalid render pass"); + ghoul_assert( + feature.type == RenderType::Polygon, + "Trying to render faulty geometry" + ); + + _linesAndPolygonsProgram->setUniform("color", _properties.fillColor()); + _linesAndPolygonsProgram->setUniform("performShading", _properties.performShading()); + + if (shouldRenderTwice) { + glEnable(GL_CULL_FACE); + // First draw back faces, then front faces + glCullFace(renderPass == 0 ? GL_FRONT : GL_BACK); + } + else { + glDisable(GL_CULL_FACE); + } + glDrawArrays(GL_TRIANGLES, 0, static_cast(feature.nVertices)); +} + +bool GlobeGeometryFeature::shouldUpdateDueToHeightMapChange() const { + if (_properties.altitudeMode() == GeoJsonProperties::AltitudeMode::RelativeToGround) { + // Cap the update to a given time interval + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + if (now - _lastHeightUpdateTime < HeightUpdateInterval) { + return false; + } + + // TODO: Change computation so that we return true immediately if even one height value is different + + // Check if last height values for the control positions have changed + std::vector newHeights = getCurrentReferencePointsHeights(); + + bool isSame = std::equal( + _lastControlHeights.begin(), + _lastControlHeights.end(), + newHeights.begin(), + newHeights.end(), + [](double a, double b) { + return std::abs(a - b) < std::numeric_limits::epsilon(); + } + ); + + if (!isSame) { + return true; + } + } + return false; +} + +void GlobeGeometryFeature::update(bool dataIsDirty, bool preventHeightUpdates) { + if (!preventHeightUpdates && shouldUpdateDueToHeightMapChange()) { + updateHeightsFromHeightMap(); + } + else if (dataIsDirty) { + updateGeometry(); + } + + if (_pointTexture) { + _pointTexture->update(); + } +} + +void GlobeGeometryFeature::updateGeometry() { + // Update vertex data and compute model coordinates based on globe + _renderFeatures.clear(); + + if (_type == GeometryType::Point) { + createPointGeometry(); + } + else { + std::vector> edgeVertices = createLineGeometry(); + createExtrudedGeometry(edgeVertices); + createPolygonGeometry(); + } + + // Compute new heights - to see if height map changed + _lastControlHeights = getCurrentReferencePointsHeights(); +} + +void GlobeGeometryFeature::updateHeightsFromHeightMap() { + // @TODO: do the updating piece by piece, not all in one frame + for (RenderFeature& f : _renderFeatures) { + f.heights = geometryhelper::heightMapHeightsFromGeodetic2List( + _globe, + f.vertices + ); + bufferDynamicHeightData(f); + } + + _lastHeightUpdateTime = std::chrono::system_clock::now(); +} + +std::vector> GlobeGeometryFeature::createLineGeometry() { + std::vector> resultPositions; + resultPositions.reserve(_geoCoordinates.size()); + + for (int i = 0; i < _geoCoordinates.size(); ++i) { + std::vector vertices; + std::vector positions; + vertices.reserve(_geoCoordinates[i].size() * 3); // TODO: this is not correct anymore + positions.reserve(_geoCoordinates[i].size() * 3); // TODO: this is not correct anymore + + glm::dvec3 lastPos = glm::dvec3(0.0); + double lastHeightValue = 0.0; + + bool isFirst = true; + for (const Geodetic3& geodetic : _geoCoordinates[i]) { + glm::dvec3 v = geometryhelper::computeOffsetedModelCoordinate( + geodetic, + _globe, + _offsets.x, + _offsets.y + ); + + auto addLinePos = [&vertices, &positions](glm::vec3 pos) { + vertices.push_back({ pos.x, pos.y, pos.z, 0.f, 0.f, 0.f }); + positions.push_back(pos); + }; + + if (isFirst) { + lastPos = v; + lastHeightValue = geodetic.height; + isFirst = false; + addLinePos(glm::vec3(v)); + continue; + } + + float length = static_cast(glm::distance(lastPos, v)); + + if (_properties.tessellationEnabled()) { + // Tessellate. + // But first, determine the step size for the tessellation (larger + // features will not be tesselated) + float stepSize = tessellationStepSize(); + + std::vector subdividedPositions = + geometryhelper::subdivideLine( + lastPos, + v, + lastHeightValue, + geodetic.height, + stepSize + ); + + // Don't add the first position. Has been added as last in previous step + for (int si = 1; si < subdividedPositions.size(); ++si) { + const geometryhelper::PosHeightPair& pair = subdividedPositions[si]; + addLinePos(glm::vec3(pair.position)); + } + } + else { + // Just add the line point + addLinePos(glm::vec3(v)); + } + + lastPos = v; + lastHeightValue = geodetic.height; + } + + vertices.shrink_to_fit(); + + RenderFeature feature; + feature.nVertices = vertices.size(); + feature.type = RenderType::Lines; + initializeRenderFeature(feature, vertices); + _renderFeatures.push_back(std::move(feature)); + + positions.shrink_to_fit(); + resultPositions.push_back(std::move(positions)); + } + + resultPositions.shrink_to_fit(); + return resultPositions; +} + +void GlobeGeometryFeature::createPointGeometry() { + if (_type != GeometryType::Point) { + return; + } + + for (size_t i = 0; i < _geoCoordinates.size(); ++i) { + std::vector vertices; + vertices.reserve(_geoCoordinates[i].size()); + + std::vector extrudedLineVertices; + extrudedLineVertices.reserve(2 * _geoCoordinates[i].size()); + + for (const Geodetic3& geodetic : _geoCoordinates[i]) { + glm::dvec3 v = geometryhelper::computeOffsetedModelCoordinate( + geodetic, + _globe, + _offsets.x, + _offsets.y + ); + + glm::vec3 vf = static_cast(v); + // Normal is the out direction + glm::vec3 normal = glm::normalize(vf); + + vertices.push_back({ vf.x, vf.y, vf.z, normal.x, normal.y, normal.z }); + + // Lines from center of the globe out to the point + extrudedLineVertices.push_back({ 0.f, 0.f, 0.f, 0.f, 0.f, 0.f }); + extrudedLineVertices.push_back({ vf.x, vf.y, vf.z, 0.f, 0.f, 0.f }); + } + + vertices.shrink_to_fit(); + extrudedLineVertices.shrink_to_fit(); + + RenderFeature feature; + feature.nVertices = vertices.size(); + feature.type = RenderType::Points; + initializeRenderFeature(feature, vertices); + _renderFeatures.push_back(std::move(feature)); + + // Create extrusion feature + RenderFeature extrudeFeature; + extrudeFeature.nVertices = extrudedLineVertices.size(); + extrudeFeature.type = RenderType::Lines; + extrudeFeature.isExtrusionFeature = true; + initializeRenderFeature(extrudeFeature, extrudedLineVertices); + _renderFeatures.push_back(std::move(extrudeFeature)); + } +} + +void GlobeGeometryFeature::createExtrudedGeometry( + const std::vector>& edgeVertices) +{ + if (edgeVertices.empty()) { + return; + } + + std::vector vertices = + geometryhelper::createExtrudedGeometryVertices(edgeVertices); + + RenderFeature feature; + feature.type = RenderType::Polygon; + feature.nVertices = vertices.size(); + feature.isExtrusionFeature = true; + initializeRenderFeature(feature, vertices); + _renderFeatures.push_back(std::move(feature)); +} + +void GlobeGeometryFeature::createPolygonGeometry() { + if (_triangleCoordinates.empty()) { + return; + } + + std::vector polyVertices; + + // Create polygon vertices from the triangle coordinates + int triIndex = 0; + std::array triPositions; + std::array triHeights; + for (const Geodetic3& geodetic : _triangleCoordinates) { + const glm::vec3 vert = geometryhelper::computeOffsetedModelCoordinate( + geodetic, + _globe, + _offsets.x, + _offsets.y + ); + triPositions[triIndex] = vert; + triHeights[triIndex] = geodetic.height; + triIndex++; + + // Once we have a triangle, start subdividing + if (triIndex == 3) { + triIndex = 0; + + const glm::vec3 v0 = triPositions[0]; + const glm::vec3 v1 = triPositions[1]; + const glm::vec3 v2 = triPositions[2]; + + double h0 = triHeights[0]; + double h1 = triHeights[1]; + double h2 = triHeights[2]; + + if (_properties.tessellationEnabled()) { + // First determine the step size for the tessellation (larger features + // will not be tesselated) + float stepSize = tessellationStepSize(); + + std::vector verts = geometryhelper::subdivideTriangle( + v0, v1, v2, + h0, h1, h2, + stepSize, + _globe + ); + polyVertices.insert(polyVertices.end(), verts.begin(), verts.end()); + } + else { + // Just add a triangle consisting of the three vertices + const glm::vec3 n = -glm::normalize(glm::cross(v1 - v0, v2 - v0)); + polyVertices.push_back({ v0.x, v0.y, v0.z, n.x, n.y, n.z }); + polyVertices.push_back({ v1.x, v1.y, v1.z, n.x, n.y, n.z }); + polyVertices.push_back({ v2.x, v2.y, v2.z, n.x, n.y, n.z }); + } + } + } + + RenderFeature triFeature; + triFeature.type = RenderType::Polygon; + triFeature.nVertices = polyVertices.size(); + initializeRenderFeature(triFeature, polyVertices); + _renderFeatures.push_back(std::move(triFeature)); +} + +void GlobeGeometryFeature::initializeRenderFeature(RenderFeature& feature, + const std::vector& vertices) +{ + // Get height map heights + feature.vertices = geometryhelper::geodetic2FromVertexList(_globe, vertices); + feature.heights = geometryhelper::heightMapHeightsFromGeodetic2List( + _globe, + feature.vertices + ); + + // Generate buffers and buffer data + feature.initializeBuffers(); + bufferVertexData(feature, vertices); +} + +float GlobeGeometryFeature::tessellationStepSize() const { + float distance = _properties.tessellationDistance(); + bool shouldDivideDistance = _properties.useTessellationLevel() && + _properties.tessellationLevel() > 0; + + if (shouldDivideDistance) { + distance /= static_cast(_properties.tessellationLevel()); + } + + return distance; +} + +std::vector GlobeGeometryFeature::getCurrentReferencePointsHeights() const { + std::vector newHeights; + newHeights.reserve(_heightUpdateReferencePoints.size()); + for (const Geodetic3& geo : _heightUpdateReferencePoints) { + const glm::dvec3 p = geometryhelper::computeOffsetedModelCoordinate( + geo, + _globe, + _offsets.x, + _offsets.y + ); + const SurfacePositionHandle handle = _globe.calculateSurfacePositionHandle(p); + newHeights.push_back(handle.heightToSurface); + } + return newHeights; +} + +void GlobeGeometryFeature::bufferVertexData(const RenderFeature& feature, + const std::vector& vertexData) +{ + ghoul_assert(_pointsProgram, "Shader program must be initialized"); + ghoul_assert(_linesAndPolygonsProgram, "Shader program must be initialized"); + + ghoul::opengl::ProgramObject* program = _linesAndPolygonsProgram; + if (feature.type == RenderType::Points) { + program = _pointsProgram; + } + + // Reserve space for both vertex and dynamic height information + auto fullBufferSize = vertexData.size() * (sizeof(Vertex) + sizeof(float)); + + glBindVertexArray(feature.vaoId); + glBindBuffer(GL_ARRAY_BUFFER, feature.vboId); + glBufferData( + GL_ARRAY_BUFFER, + fullBufferSize, + nullptr, + GL_STATIC_DRAW + ); + + glBufferSubData( + GL_ARRAY_BUFFER, + 0, // offset + vertexData.size() * sizeof(Vertex), // size + vertexData.data() + ); + + GLint positionAttrib = program->attributeLocation("in_position"); + glEnableVertexAttribArray(positionAttrib); + glVertexAttribPointer( + positionAttrib, + 3, + GL_FLOAT, + GL_FALSE, + sizeof(Vertex), + nullptr + ); + + GLint normalAttrib = program->attributeLocation("in_normal"); + glEnableVertexAttribArray(normalAttrib); + glVertexAttribPointer( + normalAttrib, + 3, + GL_FLOAT, + GL_FALSE, + sizeof(Vertex), + reinterpret_cast(3 * sizeof(float)) + ); + + // Put height data after all vertex data in buffer + unsigned long long endOfVertexData = vertexData.size() * sizeof(Vertex); + glBindVertexArray(feature.vaoId); + glBindBuffer(GL_ARRAY_BUFFER, feature.vboId); + glBufferSubData( + GL_ARRAY_BUFFER, + endOfVertexData, // offset + feature.heights.size() * sizeof(float), // size of all height values + feature.heights.data() + ); + + GLint heightAttrib = program->attributeLocation("in_height"); + glEnableVertexAttribArray(heightAttrib); + glVertexAttribPointer( + heightAttrib, + 1, + GL_FLOAT, + GL_FALSE, + 1 * sizeof(float), // stride (one height value) + reinterpret_cast(endOfVertexData) // start position + ); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +}; + +void GlobeGeometryFeature::bufferDynamicHeightData(const RenderFeature& feature) { + ghoul_assert(_pointsProgram, "Shader program must be initialized"); + ghoul_assert(_linesAndPolygonsProgram, "Shader program must be initialized"); + + ghoul::opengl::ProgramObject* program = _linesAndPolygonsProgram; + if (feature.type == RenderType::Points) { + program = _pointsProgram; + } + + // Just update the height data + + glBindVertexArray(feature.vaoId); + glBindBuffer(GL_ARRAY_BUFFER, feature.vboId); + glBufferSubData( + GL_ARRAY_BUFFER, + feature.nVertices * sizeof(Vertex), // offset + feature.heights.size() * sizeof(float), // size + feature.heights.data() + ); + + GLint heightAttrib = program->attributeLocation("in_height"); + glEnableVertexAttribArray(heightAttrib); + glVertexAttribPointer( + heightAttrib, + 1, + GL_FLOAT, + GL_FALSE, + 1 * sizeof(float), // stride + reinterpret_cast(feature.nVertices * sizeof(Vertex)) // start position + ); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +}; + +} // namespace openspace::globebrowsing diff --git a/modules/globebrowsing/src/geojson/globegeometryfeature.h b/modules/globebrowsing/src/geojson/globegeometryfeature.h new file mode 100644 index 0000000000..58247defdd --- /dev/null +++ b/modules/globebrowsing/src/geojson/globegeometryfeature.h @@ -0,0 +1,220 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYFEATURE___H__ +#define __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYFEATURE___H__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace::documentation { struct Documentation; } +namespace rendering::helper { + struct LightSourceRenderData; + struct VertexXYZNormal; +} // namespace rendering::helper + +namespace geos::geom { class Geometry; } + +namespace openspace::globebrowsing { + +class RenderableGlobe; + +/** + * This class is responsible for rendering the geomoetry features of globes, + * created e.g. from GeoJson files + */ +class GlobeGeometryFeature { +public: + GlobeGeometryFeature(const RenderableGlobe& globe, + GeoJsonProperties& defaultProperties, + GeoJsonOverrideProperties& overrideProperties); + + using Vertex = rendering::helper::VertexXYZNormal; + + // TODO: Use instead of numbers + //enum class RenderPass { + // GeometryOnly, + // GeometryAndShading + //}; + + enum class PointRenderMode { + AlignToCameraDir = 0, + AlignToCameraPos, + AlignToGlobeNormal, + AlignToGlobeSurface + }; + + enum class GeometryType { + LineString = 0, + Point, + Polygon, + Error + }; + + enum class RenderType { + Lines = 0, + Points, + Polygon, + Uninitialized + }; + + // Each geometry feature might translate into several render features + struct RenderFeature { + void initializeBuffers(); + + RenderType type = RenderType::Uninitialized; + GLuint vaoId = 0; + GLuint vboId = 0; + size_t nVertices = 0; + bool isExtrusionFeature = false; + + // Store the geodetic lat long coordinates of each vertex, so we can quickly recompute + // the height values for these points + std::vector vertices; + + // Keep the heights around + std::vector heights; + }; + + // Some extra data that we need for doing the rendering + struct ExtraRenderData { + float pointSizeScale; + float lineWidthScale; + PointRenderMode& pointRenderMode; + rendering::helper::LightSourceRenderData& lightSourceData; + }; + + std::string key() const; + + void setOffsets(const glm::vec3& offsets); + + void initializeGL(ghoul::opengl::ProgramObject* pointsProgram, + ghoul::opengl::ProgramObject* linesAndPolygonsProgram); + void deinitializeGL(); + bool isReady() const; + bool isPoints() const; + bool useHeightMap() const; + + void updateTexture(bool isInitializeStep = false); + + void createFromSingleGeosGeometry(const geos::geom::Geometry* geo, int index, + bool ignoreHeights); + + // 2 pass rendering to get correct culling for polygons + void render(const RenderData& renderData, int pass, float mainOpacity, + const ExtraRenderData& extraRenderData); + + bool shouldUpdateDueToHeightMapChange() const; + + void update(bool dataIsDirty, bool preventHeightUpdates); + void updateGeometry(); + void updateHeightsFromHeightMap(); + +private: + void renderPoints(const RenderFeature& feature, const RenderData& renderData, + const PointRenderMode& renderMode, float sizeScale) const; + + void renderLines(const RenderFeature& feature) const; + + void renderPolygons(const RenderFeature& feature, bool shouldRenderTwice, + int renderPass) const; + + /** + * Create the vertex information for any line parts of the feature. + * Returns the resulting vertex positions, so we can use them for extrusion + */ + std::vector> createLineGeometry(); + + /** + * Create the vertex information for any point parts of the feature. Also creates + * the features for extruded lines for the points + */ + void createPointGeometry(); + + /** + * Create the triangle geometry for the extruded edges of lines/polygons + */ + void createExtrudedGeometry(const std::vector>& edgeVertices); + + /** + * Create the triangle geometry for the polygon part of the feature (the area + * contained by the shape) + */ + void createPolygonGeometry(); + + void initializeRenderFeature(RenderFeature& feature, + const std::vector& vertices); + + /// Get the distance that shall be used for tesselation, based on the properties + float tessellationStepSize() const; + + /// Compute the heights to the surface at the reference points + std::vector getCurrentReferencePointsHeights() const; + + /// Buffer the static data for the vertices + void bufferVertexData(const RenderFeature& feature, + const std::vector& vertexData); + + /// Buffer the dynamic height data for the vertices, based on the height map + void bufferDynamicHeightData(const RenderFeature& feature); + + GeometryType _type = GeometryType::Error; + const RenderableGlobe& _globe; + + // Coordinates for geometry. For polygons, the first is always the outer ring + // and any following are the inner rings (holes) + std::vector> _geoCoordinates; + + // Coordinates for any triangles representing the geometry (only relevant for polygons) + std::vector _triangleCoordinates; + + std::vector _renderFeatures; + + glm::vec3 _offsets = glm::vec3(0.f); // lat, long, distance (meters). Passed from parent on property change + + std::string _key; + const PropertySet _properties; + + std::vector _heightUpdateReferencePoints; + std::vector _lastControlHeights; + std::chrono::system_clock::time_point _lastHeightUpdateTime; + + bool _hasTexture = false; + std::unique_ptr _pointTexture; + + ghoul::opengl::ProgramObject* _linesAndPolygonsProgram = nullptr; + ghoul::opengl::ProgramObject* _pointsProgram = nullptr; +}; + +} // namespace openspace::globebrowsing + +#endif // __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYFEATURE___H__ diff --git a/modules/globebrowsing/src/geojson/globegeometryhelper.cpp b/modules/globebrowsing/src/geojson/globegeometryhelper.cpp new file mode 100644 index 0000000000..190058a571 --- /dev/null +++ b/modules/globebrowsing/src/geojson/globegeometryhelper.cpp @@ -0,0 +1,360 @@ +/***************************************************************************************** + * * + * 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 + +namespace openspace::globebrowsing::geometryhelper { + +Geodetic3 toGeodetic(const geos::geom::Coordinate& c) { + Geodetic3 gd; + gd.geodetic2.lon = glm::radians(c.x); + gd.geodetic2.lat = glm::radians(c.y); + gd.height = std::isnan(c.z) ? 0.0 : c.z; + return gd; +} + +geos::geom::Coordinate toGeosCoord(const Geodetic3& gd) { + geos::geom::Coordinate c; + c.x = glm::degrees(gd.geodetic2.lon); + c.y = glm::degrees(gd.geodetic2.lat); + c.z = gd.height; + return c; +} + +std::vector +coordsToGeodetic(const std::vector& coords) +{ + std::vector res; + res.reserve(coords.size()); + for (const geos::geom::Coordinate& c : coords) { + res.push_back(toGeodetic(c)); + } + return res; +} + +std::vector geometryCoordsAsGeoVector(const geos::geom::Geometry* geometry) { + std::vector coords; + geometry->getCoordinates()->toVector(coords); + return geometryhelper::coordsToGeodetic(coords); +} + +std::vector geodetic2FromVertexList(const RenderableGlobe& globe, + const std::vector& verts) +{ + std::vector res; + res.reserve(verts.size()); + for (const rendering::helper::VertexXYZNormal& v : verts) { + glm::dvec3 cartesian = glm::dvec3(v.xyz[0], v.xyz[1], v.xyz[2]); + res.push_back(globe.ellipsoid().cartesianToGeodetic2(cartesian)); + } + return res; +} + +std::vector heightMapHeightsFromGeodetic2List(const RenderableGlobe& globe, + const std::vector& list) +{ + std::vector res; + res.reserve(list.size()); + for (const Geodetic2& geo : list) { + float h = static_cast(getHeightToReferenceSurface(geo, globe)); + res.push_back(h); + } + return res; +} + +std::vector +createExtrudedGeometryVertices(const std::vector>& edgeVertices) +{ + std::vector vertices; + + size_t nVerts = 0; + for (const std::vector& edge : edgeVertices) { + nVerts += edge.size(); + } + vertices.reserve(nVerts * 3); + + // Extrude polygon + for (size_t nBound = 0; nBound < edgeVertices.size(); ++nBound) { + const std::vector& boundary = edgeVertices[nBound]; + for (int i = 1; i < boundary.size(); ++i) { + glm::vec3 v0 = boundary[i - 1]; + glm::vec3 v1 = boundary[i ]; + + // Vertices close to globe (Based on origin which is the zero point here) + // For now, use center of globe (TODO: allow setting the height) + glm::vec3 vOrigin = glm::vec3(0.f); + + // Outer boundary is the first one + if (nBound == 0) { + glm::vec3 n = glm::vec3(glm::normalize(glm::cross(v1 - vOrigin, v0 - vOrigin))); + vertices.push_back({ vOrigin.x, vOrigin.y, vOrigin.z, n.x, n.y, n.z }); + vertices.push_back({ v1.x, v1.y, v1.z, n.x, n.y, n.z }); + vertices.push_back({ v0.x, v0.y, v0.z, n.x, n.y, n.z }); + } + // Inner boundary + else { + // Flipped winding order and normal for inner rings + glm::vec3 n = glm::normalize(glm::cross(v0 - vOrigin, v1 - vOrigin)); + vertices.push_back({ vOrigin.x, vOrigin.y, vOrigin.z, n.x, n.y, n.z }); + vertices.push_back({ v0.x, v0.y, v0.z, n.x, n.y, n.z }); + vertices.push_back({ v1.x, v1.y, v1.z, n.x, n.y, n.z }); + } + + // TODO: Fix faulty triangle directions and draw triangles with correct shading on + // both sides of the mesh + } + } + + vertices.shrink_to_fit(); + return vertices; + + // TODO: extrude lines as a box shape +} + +double getHeightToReferenceSurface(const Geodetic2& geo, const RenderableGlobe& globe) { + // Compute model space coordinate, and potentially account for height map + glm::dvec3 posModelSpaceZeroHeight = + globe.ellipsoid().cartesianSurfacePosition(geo); + + const SurfacePositionHandle posHandle = + globe.calculateSurfacePositionHandle(posModelSpaceZeroHeight); + + return posHandle.heightToSurface; +} + +glm::dvec3 computeOffsetedModelCoordinate(const Geodetic3& geo, + const RenderableGlobe& globe, + float latOffset, float lonOffset) +{ + // Account for lat long offset + double offsetLatRadians = glm::radians(latOffset); + double offsetLonRadians = glm::radians(lonOffset); + + Geodetic3 adjusted = geo; + adjusted.geodetic2.lat += offsetLatRadians; + adjusted.geodetic2.lon += offsetLonRadians; + + return globe.ellipsoid().cartesianPosition(adjusted); +} + +std::vector subdivideLine(const glm::dvec3& v0, const glm::dvec3& v1, + double h0, double h1, double maxDistance) +{ + double edgeLength = glm::distance(v1, v0); + int nSegments = static_cast(std::ceil(edgeLength / maxDistance)); + + std::vector positions; + positions.reserve(nSegments + 1); + + // If step distance is too big, just add first position + if (nSegments == 0) { + positions.push_back({ glm::vec3(v0), h0 }); + } + + for (int seg = 0; seg < nSegments; ++seg) { + double t = static_cast(seg) / static_cast(nSegments); + + // Interpolate both position and height value + glm::dvec3 newV = glm::mix(v0, v1, t); + double newHeight = glm::mix(h0, h1, t); + double heightDiff = newHeight - h0; + + // Compute position along arc between v0 and v1, with adjusted height value + glm::vec3 newVf = static_cast( + (glm::length(v0) + heightDiff) * glm::normalize(newV) + ); + positions.push_back({ newVf, newHeight }); + } + + // Add final position + positions.push_back({ static_cast(v1), h1 }); + + positions.shrink_to_fit(); + return positions; +} + +std::vector +subdivideTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, + double h0, double h1, double h2, double maxDistance, + const RenderableGlobe& globe) +{ + std::vector vertices; + + // Subdivide edges + std::vector edge01 = geometryhelper::subdivideLine( + v0, v1, + h0, h1, + maxDistance + ); + + std::vector edge02 = geometryhelper::subdivideLine( + v0, v2, + h0, h2, + maxDistance + ); + + std::vector edge12 = geometryhelper::subdivideLine( + v1, v2, + h1, h2, + maxDistance + ); + + size_t nSteps01 = edge01.size(); + size_t nSteps02 = edge02.size(); + size_t nSteps12 = edge12.size(); + size_t maxSteps = std::max(std::max(nSteps01, nSteps02), nSteps12); + vertices.reserve(maxSteps * maxSteps); + + // Add points inside the triangle + std::vector pointCoords; + pointCoords.reserve(3 * maxSteps + 1); + + const float lengthEdge01 = glm::length(v1 - v0); + const float lengthEdge02 = glm::length(v2 - v0); + for (size_t i = 1; i < nSteps01; ++i) { + for (size_t j = 1; j < nSteps02; ++j) { + glm::vec3 comp01 = edge01[i].position - v0; + glm::vec3 comp02 = edge02[j].position - v0; + + double hComp01 = edge01[i].height - h0; + double hComp02 = edge02[j].height - h0; + + float w1 = glm::length(comp01) / lengthEdge01; + float w2 = glm::length(comp02) / lengthEdge02; + + if (w1 + w2 > 1.f - std::numeric_limits::epsilon()) { + continue; // Sum larger than 1.0 => Outside of triangle + } + + glm::vec3 pos = v0 + comp01 + comp02; + double height = h0 + hComp01 + hComp02; + + Geodetic2 geo2 = globe.ellipsoid().cartesianToGeodetic2(pos); + Geodetic3 geo3 = { geo2, height }; + pointCoords.push_back(geometryhelper::toGeosCoord(geo3)); + } + } + + // Add egde positions + for (size_t i = 0; i < maxSteps; ++i) { + if (i < edge01.size() - 1) { + Geodetic2 geo2 = globe.ellipsoid().cartesianToGeodetic2(edge01[i].position); + Geodetic3 geo3 = { geo2, edge01[i].height }; + pointCoords.push_back(geometryhelper::toGeosCoord(geo3)); + } + if (i < edge02.size() - 1) { + Geodetic2 geo2 = globe.ellipsoid().cartesianToGeodetic2(edge02[i].position); + Geodetic3 geo3 = { geo2, edge02[i].height }; + pointCoords.push_back(geometryhelper::toGeosCoord(geo3)); + } + if (i < edge12.size() - 1) { + Geodetic2 geo2 = globe.ellipsoid().cartesianToGeodetic2(edge12[i].position); + Geodetic3 geo3 = { geo2, edge12[i].height }; + pointCoords.push_back(geometryhelper::toGeosCoord(geo3)); + } + } + + // Also add the final position (not part of the subdivide step above) + Geodetic2 geo2 = globe.ellipsoid().cartesianToGeodetic2(v2); + glm::dvec3 centerToEllipsoidSurface = globe.ellipsoid().geodeticSurfaceProjection(v2); + double height = glm::length(glm::dvec3(v2) - centerToEllipsoidSurface); + Geodetic3 geo3 = { geo2, height }; + pointCoords.push_back(geometryhelper::toGeosCoord(geo3)); + + pointCoords.shrink_to_fit(); + + using namespace geos::geom; + + GeometryFactory::Ptr geometryFactory = GeometryFactory::create(); + std::unique_ptr points = geometryFactory->createMultiPoint(pointCoords); + + // Create triangulation of points + geos::triangulate::DelaunayTriangulationBuilder builder; + builder.setSites(*points->getCoordinates()); + + // Returns a list of triangles, as geos polygons + GeometryCollection* triangleGeoms = builder.getTriangles(*geometryFactory).release(); + std::vector triCoords; + triangleGeoms->getCoordinates()->toVector(triCoords); + + vertices.reserve(vertices.size() + triCoords.size() + 1); + + int count = 0; + for (const Coordinate& coord : triCoords) { + count++; + if (count == 4) { + // Skip every 4th coord, as polygons have one extra coord per triangle. + // Also, reset the counting at this point. + count = 0; + continue; + } + Geodetic3 geodetic = geometryhelper::toGeodetic(coord); + + // Note that offset should already have been applied to the coordinates. Use + // zero offset => just get model coordinate + glm::vec3 v = + geometryhelper::computeOffsetedModelCoordinate(geodetic, globe, 0.f, 0.f); + + vertices.push_back({ v.x, v.y, v.z, 0.f, 0.f, 0.f }); + + // Every third set of coordinates is a triangle => update normal of previous + // triangle vertices + if (count == 3) { + // Find previous vertices + size_t lastIndex = vertices.size() - 1; + rendering::helper::VertexXYZNormal& vert0 = vertices[lastIndex - 2]; + rendering::helper::VertexXYZNormal& vert1 = vertices[lastIndex - 1]; + rendering::helper::VertexXYZNormal& vert2 = vertices[lastIndex]; + + const glm::vec3 v0_pos = glm::vec3(vert0.xyz[0], vert0.xyz[1], vert0.xyz[2]); + const glm::vec3 v1_pos = glm::vec3(vert1.xyz[0], vert1.xyz[1], vert1.xyz[2]); + const glm::vec3 n = -glm::normalize(glm::cross(v1_pos - v0_pos, v - v0_pos)); + + vert0.normal[0] = n.x; + vert0.normal[1] = n.y; + vert0.normal[2] = n.z; + + vert1.normal[0] = n.x; + vert1.normal[1] = n.y; + vert1.normal[2] = n.z; + + vert2.normal[0] = n.x; + vert2.normal[1] = n.y; + vert2.normal[2] = n.z; + } + } + + vertices.shrink_to_fit(); + return vertices; +} + +} // namespace openspace::globebrowsing::geometryhelper diff --git a/modules/globebrowsing/src/geojson/globegeometryhelper.h b/modules/globebrowsing/src/geojson/globegeometryhelper.h new file mode 100644 index 0000000000..736347bdbe --- /dev/null +++ b/modules/globebrowsing/src/geojson/globegeometryhelper.h @@ -0,0 +1,109 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYHELPER___H__ +#define __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYHELPER___H__ + +#include +#include + +namespace openspace::globebrowsing { + struct Geodetic2; + struct Geodetic3; + class RenderableGlobe; +} // namespace openspace::globebrowsing + +namespace openspace::rendering::helper { + struct VertexXYZNormal; +} // namespace openspace::rendering::helper + +namespace geos::geom { + class Coordinate; + class Geometry; +} // namespace geos::geom + +namespace openspace::globebrowsing::geometryhelper { + +Geodetic3 toGeodetic(const geos::geom::Coordinate& c); + +geos::geom::Coordinate toGeosCoord(const Geodetic3& gd); + +std::vector coordsToGeodetic( + const std::vector& coords); + +std::vector geometryCoordsAsGeoVector(const geos::geom::Geometry* geometry); + +std::vector geodetic2FromVertexList(const RenderableGlobe& globe, + const std::vector& verts); + +std::vector heightMapHeightsFromGeodetic2List(const RenderableGlobe& globe, + const std::vector& list); + + +/** + * Create triangle geometry for the extruded edge, given the provided edge vertices + */ +std::vector createExtrudedGeometryVertices( + const std::vector>& edgeVertices); + +/** + * Get height contribution from reference surface of the globe, based on the height map + */ +double getHeightToReferenceSurface(const Geodetic2& geo, const RenderableGlobe& globe); + +/** + * Compute model space cordinate from geodetic coordinate, and account for lat, long + * offsets + */ +glm::dvec3 computeOffsetedModelCoordinate(const Geodetic3& geo, + const RenderableGlobe& globe, float latOffset, float lonOffset); + + +struct PosHeightPair { + glm::vec3 position; + double height; +}; + +/** + * Subdivide line between position v0 and v1 so that it fullfils the maxDistance + * criteria. Interpolate the height value from * h0 to h1, as well as add the + * given offset and account for the height map if that should be done. + * + * Returns pairs of position and height values + */ +std::vector subdivideLine(const glm::dvec3& v0, const glm::dvec3& v1, + double h0, double h1, double maxDistance); + +/** + * Subdivide triangle consisting of vertex positions v0, v1 and v2, with height values + * h0, h1 and h2 into smaller triangles. maxDistance specifies tha maximum distance + * between two vertices in the subdivided mesh + */ +std::vector subdivideTriangle( + const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, + double h0, double h1, double h2, double maxDistance, const RenderableGlobe& globe); + +} // namespace openspace::globebrowsing::geometryhelper + +#endif // __OPENSPACE_MODULE_GLOBEBROWSING___GLOBEGEOMETRYHELPER___H__ diff --git a/modules/globebrowsing/src/renderableglobe.cpp b/modules/globebrowsing/src/renderableglobe.cpp index 689b93089b..deddea41b8 100644 --- a/modules/globebrowsing/src/renderableglobe.cpp +++ b/modules/globebrowsing/src/renderableglobe.cpp @@ -677,6 +677,9 @@ RenderableGlobe::RenderableGlobe(const ghoul::Dictionary& dictionary) _labelsDictionary = p.labels.value_or(_labelsDictionary); + // Init geojson manager + _geoJsonManager.initialize(this); + addPropertySubOwner(_geoJsonManager); // Components _hasRings = p.rings.has_value(); @@ -740,6 +743,8 @@ void RenderableGlobe::deinitializeGL() { _globalRenderer.program = nullptr; } + _geoJsonManager.deinitializeGL(); + _grid.deinitializeGL(); if (_hasRings) { @@ -856,6 +861,10 @@ void RenderableGlobe::renderSecondary(const RenderData& data, RendererTasks&) { catch (const ghoul::opengl::TextureUnit::TextureUnitError& e) { LERROR(fmt::format("Error on drawing globe labels: '{}'", e.message)); } + + if (_geoJsonManager.isReady()) { + _geoJsonManager.render(data); + } } void RenderableGlobe::update(const UpdateData& data) { @@ -936,6 +945,8 @@ void RenderableGlobe::update(const UpdateData& data) { // RenderableGlobe::render() // rendering with the new number of layers but the // // LayerManager hasn't updated yet :o _layerManagerDirty = true; + + _geoJsonManager.update(); } bool RenderableGlobe::renderedWithDesiredData() const { @@ -950,6 +961,14 @@ LayerManager& RenderableGlobe::layerManager() { return _layerManager; } +const GeoJsonManager& RenderableGlobe::geoJsonManager() const { + return _geoJsonManager; +} + +GeoJsonManager& RenderableGlobe::geoJsonManager() { + return _geoJsonManager; +} + const Ellipsoid& RenderableGlobe::ellipsoid() const { return _ellipsoid; } diff --git a/modules/globebrowsing/src/renderableglobe.h b/modules/globebrowsing/src/renderableglobe.h index 32d31c814d..e6ed5c1c61 100644 --- a/modules/globebrowsing/src/renderableglobe.h +++ b/modules/globebrowsing/src/renderableglobe.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -114,6 +115,9 @@ public: const Ellipsoid& ellipsoid() const; const LayerManager& layerManager() const; LayerManager& layerManager(); + const GeoJsonManager& geoJsonManager() const; + GeoJsonManager& geoJsonManager(); + const glm::dmat4& modelTransform() const; static documentation::Documentation Documentation(); @@ -252,6 +256,8 @@ private: SkirtedGrid _grid; LayerManager _layerManager; + GeoJsonManager _geoJsonManager; + glm::dmat4 _cachedModelTransform = glm::dmat4(1.0); glm::dmat4 _cachedInverseModelTransform = glm::dmat4(1.0); diff --git a/modules/imgui/ext/imgui/imgui_demo.cpp b/modules/imgui/ext/imgui/imgui_demo.cpp index 90e91aa057..34c569c034 100644 --- a/modules/imgui/ext/imgui/imgui_demo.cpp +++ b/modules/imgui/ext/imgui/imgui_demo.cpp @@ -6049,7 +6049,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::EndTooltip(); } ImGui::SameLine(); - HelpMarker("When drawing circle primitives with \"num_segments == 0\" tesselation will be calculated automatically."); + HelpMarker("When drawing circle primitives with \"num_segments == 0\" tessellation will be calculated automatically."); ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero. ImGui::PopItemWidth(); diff --git a/openspace.cfg b/openspace.cfg index 6806e32345..888fb2114e 100644 --- a/openspace.cfg +++ b/openspace.cfg @@ -161,7 +161,8 @@ ModuleConfigurations = { GlobeBrowsing = { TileCacheSize = 2048, -- for all globes (CPU and GPU memory) MRFCacheEnabled = false, - MRFCacheLocation = "${BASE}/mrf_cache" + MRFCacheLocation = "${BASE}/mrf_cache", + DefaultGeoPointTexture = "${DATA}/globe_pin.png" }, Sync = { SynchronizationRoot = "${SYNC}", diff --git a/scripts/drag_drop_handler.lua b/scripts/drag_drop_handler.lua index e1b58c7473..b972971e8e 100644 --- a/scripts/drag_drop_handler.lua +++ b/scripts/drag_drop_handler.lua @@ -46,4 +46,6 @@ elseif extension == ".asset" then openspace.asset.add("]] .. filename .. '");' .. ReloadUIScript elseif extension == ".osrec" or extension == ".osrectxt" then return 'openspace.sessionRecording.startPlayback("' .. filename .. '")' +elseif extension == ".geojson" then + return 'openspace.globebrowsing.addGeoJsonFromFile("' .. filename .. '")' .. ReloadUIScript end diff --git a/src/rendering/helper.cpp b/src/rendering/helper.cpp index 61c841be35..8184c7b605 100644 --- a/src/rendering/helper.cpp +++ b/src/rendering/helper.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -504,4 +505,26 @@ std::pair, std::vector> createSphere(int nSegments return { vertices, indices }; } +void LightSourceRenderData::updateBasedOnLightSources(const RenderData& renderData, + const std::vector>& sources) +{ + unsigned int nEnabledLightSources = 0; + intensitiesBuffer.resize(sources.size()); + directionsViewSpaceBuffer.resize(sources.size()); + + // Get intensities and view space direction for the given light sources, + // given the provided render data information + for (const std::unique_ptr& lightSource : sources) { + if (!lightSource->isEnabled()) { + continue; + } + intensitiesBuffer[nEnabledLightSources] = lightSource->intensity(); + directionsViewSpaceBuffer[nEnabledLightSources] = + lightSource->directionViewSpace(renderData); + + ++nEnabledLightSources; + } + nLightSources = nEnabledLightSources; +} + } // namespace openspace::rendering::helper diff --git a/src/rendering/texturecomponent.cpp b/src/rendering/texturecomponent.cpp index ae5b7ea948..61432631b3 100644 --- a/src/rendering/texturecomponent.cpp +++ b/src/rendering/texturecomponent.cpp @@ -90,16 +90,19 @@ void TextureComponent::loadFromFile(const std::filesystem::path& path) { if (!path.empty()) { using namespace ghoul::io; using namespace ghoul::opengl; + + std::filesystem::path absolutePath = absPath(path); + std::unique_ptr texture = TextureReader::ref().loadTexture( - absPath(path.string()).string(), + absolutePath.string(), _nDimensions ); if (texture) { - LDEBUG(fmt::format("Loaded texture from {}", absPath(path.string()))); + LDEBUG(fmt::format("Loaded texture from {}", absolutePath)); _texture = std::move(texture); - _textureFile = std::make_unique(path); + _textureFile = std::make_unique(absolutePath); if (_shouldWatchFile) { _textureFile->setCallback([this]() { _fileIsDirty = true; }); }