From 87c13727449faa7b387faffc10c41bea4c7e3986 Mon Sep 17 00:00:00 2001 From: ElonOlsson Date: Thu, 12 Jun 2025 09:57:26 -0400 Subject: [PATCH] Todays Sun, PR for branch: feature/WSA (#3652) The merge commit --------- Co-authored-by: shyamthyagarajan Co-authored-by: Lundkvist Co-authored-by: lundkvistarn Co-authored-by: Alexander Bock Co-authored-by: Emma Broman Co-authored-by: Alexander Bock --- .../sun_earth_2012_fieldlines_batsrus.asset | 13 +- .../sun_earth_2012_fieldlines_enlil.asset | 18 +- .../2012/sun_earth_2012_fieldlines_pfss.asset | 3 +- .../carrington_to_heeq_rotation.asset | 26 - .../heliosphere/bastille_day/fieldlines.asset | 19 +- .../heliosphere/bastille_day/fluxnodes.asset | 8 +- .../bastille_day/fluxnodescutplane.asset | 11 +- .../heliosphere/todayssun/fieldlines.asset | 203 +++ .../heliosphere/todayssun/grid.asset | 128 ++ .../heliosphere/todayssun/mission.asset | 65 + .../todayssun/sun_earth_line.asset | 43 + .../heliosphere/todayssun/surfaces.asset | 283 ++++ .../transforms_heliosphere.asset | 24 +- .../missions/juice/fieldlines.asset | 6 +- .../sun/{EUV_layer.asset => euv_layer.asset} | 2 +- .../spaceweather/bastilleday2000.profile | 2 +- data/profiles/spaceweather/todays_sun.profile | 164 ++ include/openspace/properties/propertyowner.h | 2 +- .../util/dynamicfilesequencedownloader.h | 107 ++ modules/base/rendering/renderablesphere.cpp | 70 +- modules/base/rendering/renderablesphere.h | 9 +- .../rendering/renderabletimevaryingsphere.cpp | 21 +- .../rendering/renderabletimevaryingsphere.h | 2 - modules/base/shaders/sphere_fs.glsl | 21 +- modules/exoplanets/exoplanetsmodule_lua.inl | 6 +- .../tasks/exoplanetsdatapreparationtask.h | 2 +- modules/fieldlinessequence/CMakeLists.txt | 2 + .../fieldlinessequencemodule.cpp | 17 +- modules/fieldlinessequence/include.cmake | 1 + .../renderablefieldlinessequence.cpp | 1439 +++++++++-------- .../rendering/renderablefieldlinessequence.h | 208 +-- .../shaders/fieldlinessequence_vs.glsl | 10 +- .../tasks/kameleonvolumetofieldlinestask.cpp | 204 +++ .../tasks/kameleonvolumetofieldlinestask.h | 64 + .../util/fieldlinesstate.cpp | 48 +- .../fieldlinessequence/util/fieldlinesstate.h | 10 +- .../util/kameleonfieldlinehelper.cpp | 135 +- .../util/kameleonfieldlinehelper.h | 13 + modules/fitsfilereader/CMakeLists.txt | 4 + .../fitsfilereader/fitsfilereadermodule.cpp | 21 + modules/fitsfilereader/fitsfilereadermodule.h | 5 + .../include/renderabletimevaryingfitssphere.h | 127 ++ .../fitsfilereader/include/wsafitshelper.h | 75 + .../src/renderabletimevaryingfitssphere.cpp | 678 ++++++++ modules/fitsfilereader/src/wsafitshelper.cpp | 156 ++ modules/gaia/rendering/octreemanager.h | 4 +- .../kameleonvolume/kameleonvolumereader.cpp | 3 +- .../server/include/logging/notificationlog.h | 2 +- .../space/rendering/renderablefluxnodes.cpp | 2 +- src/CMakeLists.txt | 2 + src/navigation/navigationhandler_lua.inl | 14 +- src/util/dynamicfilesequencedownloader.cpp | 649 ++++++++ src/util/httprequest.cpp | 5 +- 53 files changed, 4225 insertions(+), 931 deletions(-) delete mode 100644 data/assets/scene/solarsystem/heliosphere/bastille_day/carrington_to_heeq_rotation.asset create mode 100644 data/assets/scene/solarsystem/heliosphere/todayssun/fieldlines.asset create mode 100644 data/assets/scene/solarsystem/heliosphere/todayssun/grid.asset create mode 100644 data/assets/scene/solarsystem/heliosphere/todayssun/mission.asset create mode 100644 data/assets/scene/solarsystem/heliosphere/todayssun/sun_earth_line.asset create mode 100644 data/assets/scene/solarsystem/heliosphere/todayssun/surfaces.asset rename data/assets/scene/solarsystem/{sun => heliosphere}/transforms_heliosphere.asset (86%) rename data/assets/scene/solarsystem/sun/{EUV_layer.asset => euv_layer.asset} (98%) create mode 100644 data/profiles/spaceweather/todays_sun.profile create mode 100644 include/openspace/util/dynamicfilesequencedownloader.h create mode 100644 modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.cpp create mode 100644 modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.h create mode 100644 modules/fitsfilereader/include/renderabletimevaryingfitssphere.h create mode 100644 modules/fitsfilereader/include/wsafitshelper.h create mode 100644 modules/fitsfilereader/src/renderabletimevaryingfitssphere.cpp create mode 100644 modules/fitsfilereader/src/wsafitshelper.cpp create mode 100644 src/util/dynamicfilesequencedownloader.cpp diff --git a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_batsrus.asset b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_batsrus.asset index 6a34d700e4..acf6983973 100644 --- a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_batsrus.asset +++ b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_batsrus.asset @@ -65,8 +65,9 @@ local BatsrusJ12OpenClosed = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.openClosed, InputFileType = "Osfls", - Color = { 0.7, 0.4, 0.0, 0.6 }, -- Default color - ColorMethod = "By Quantity", -- Color by Quantity + ShowAtAllTimes = false, + Color = { 0.7, 0.4, 0.0, 0.6 }, + ColorMethod = "By Quantity", ColorQuantity = 0, -- Temperature ColorTablePaths = { batsrusTemperatureColorTable, @@ -78,7 +79,7 @@ local BatsrusJ12OpenClosed = { ColorTableRanges = colorRanges, MaskingEnabled = true, MaskingQuantity = 4, -- Topology - MaskingRanges = { {2.5, 3.0} } -- Corresponds to closed fieldlines only + MaskingRanges = { { 2.5, 3.0 } } -- Corresponds to closed fieldlines only }, GUI = { Name = "Fieldlines BATSRUS J12 Open/Closed", @@ -95,9 +96,10 @@ local BatsrusJ12FlowLines = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.velocityFlow, InputFileType = "Osfls", - ColorMethod = "By Quantity", -- Color by Quantity + ShowAtAllTimes = false, + ColorMethod = "By Quantity", ColorQuantity = 3, -- Velocity - Color = { 0.7, 0.4, 0.0, 0.12 }, -- Default color + Color = { 0.7, 0.4, 0.0, 0.12 }, ColorTablePaths = { batsrusTemperatureColorTable, batsrusDensityColorTable, @@ -123,6 +125,7 @@ local BatsrusAsherStaticSeedsFlowLines = { Enabled = false, SourceFolder = unzippedDataDestination.asherStatic, InputFileType = "Osfls", + ShowAtAllTimes = false, ColorTablePaths = { batsrusTemperatureColorTable, batsrusDensityColorTable, diff --git a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_enlil.asset b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_enlil.asset index 336ec6d658..dd8d173ff9 100644 --- a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_enlil.asset +++ b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_enlil.asset @@ -1,5 +1,5 @@ asset.require("scene/solarsystem/heliosphere/2012/reset_loop_action") -local transforms = asset.require("scene/solarsystem/sun/transforms_heliosphere") +local transforms = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") @@ -61,8 +61,9 @@ local ENLILSliceEqPlane11AU1 = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.EqPlane011AU1, InputFileType = "Osfls", + ShowAtAllTimes = false, Color = { 0.4, 0.15, 0.4, 0.6 }, - ColorMethod = "By Quantity", -- Color by Quantity + ColorMethod = "By Quantity", ColorQuantity = 1, -- Velocity ColorTablePaths = { enlilDensityColorTable, @@ -84,8 +85,9 @@ local ENLILSliceEqPlane11AU2 = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.EqPlane011AU2, InputFileType = "Osfls", + ShowAtAllTimes = false, Color = { 0.4, 0.15, 0.4, 0.6 }, - ColorMethod = "By Quantity", -- Color by Quantity + ColorMethod = "By Quantity", ColorQuantity = 1, -- Velocity ColorTablePaths = { enlilDensityColorTable, @@ -107,9 +109,9 @@ local ENLILSliceLat411AU1 = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.Lat4011AU1, InputFileType = "Osfls", - + ShowAtAllTimes = false, Color = { 0.4, 0.15, 0.4, 0.6 }, - ColorMethod = "By Quantity", -- Color by Quantity + ColorMethod = "By Quantity", ColorQuantity = 1, -- Velocity ColorTablePaths = { enlilDensityColorTable, @@ -131,9 +133,9 @@ local ENLILSliceLat411AU2 = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.Lat4011AU2, InputFileType = "Osfls", - + ShowAtAllTimes = false, Color = { 0.4, 0.15, 0.4, 0.6 }, - ColorMethod = "By Quantity", -- Color by Quantity + ColorMethod = "By Quantity", ColorQuantity = 1, -- Velocity ColorTablePaths = { enlilDensityColorTable, @@ -155,6 +157,7 @@ local ENLILEarth = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.Earth, InputFileType = "Osfls", + ShowAtAllTimes = false, Color = { 1.0, 1.0, 1.0, 0.6 }, ColorTablePaths = { enlilDensityColorTable, @@ -176,6 +179,7 @@ local ENLILStereoA = { Type = "RenderableFieldlinesSequence", SourceFolder = unzippedDataDestination.StereoA, InputFileType = "Osfls", + ShowAtAllTimes = false, Color = { 1.0, 1.0, 1.0, 0.6 }, ColorTablePaths = { enlilDensityColorTable, diff --git a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_pfss.asset b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_pfss.asset index b0d83cae39..168938bf40 100644 --- a/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_pfss.asset +++ b/data/assets/scene/solarsystem/heliosphere/2012/sun_earth_2012_fieldlines_pfss.asset @@ -1,4 +1,4 @@ -local transforms = asset.require("scene/solarsystem/sun/transforms_heliosphere") +local transforms = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") @@ -53,6 +53,7 @@ local PFSS = { Type = "RenderableFieldlinesSequence", SourceFolder = PFSSPaths.SolarSoft, InputFileType = "Osfls", + ShowAtAllTimes = true, Color = { 0.35, 0.51, 0.875, 0.22 }, FlowEnabled = true, ReversedFlow = true, diff --git a/data/assets/scene/solarsystem/heliosphere/bastille_day/carrington_to_heeq_rotation.asset b/data/assets/scene/solarsystem/heliosphere/bastille_day/carrington_to_heeq_rotation.asset deleted file mode 100644 index c4607f4f9f..0000000000 --- a/data/assets/scene/solarsystem/heliosphere/bastille_day/carrington_to_heeq_rotation.asset +++ /dev/null @@ -1,26 +0,0 @@ -local CarringtonLongitudeToHEEQ180Rotation = { - -- This is a rotation matrix to go from the Carrington longitude reference frame to the - -- HEEQ180 reference frame. At the reference time, MAS_seq = 0, 2000-07-14T08:33:37.105 - -- the Carrington longitude was 309.3 degrees. - -- Difference from HEEQ => 360-309.3=50.7 (or 0-309.3 = -309.3). However this leads to - -- the same rotation matrix in the end) - -- Since OpenSpace supports HEEQ180 and not HEEQ, 180 was added or subtracted - -- => a1 = -129.3 and a2 = 230.7 - -- Rotation matrix: (cos a, -sin a, 0)(sin a, cos a, 0)(0, 0, 1) leads to the result. - Type = "FixedRotation", - XAxis = { -0.63338087262755016203262119192353, -0.77384020972650618518999944537717, 0.0 }, - YAxis = { 0.77384020972650618518999944537717, -0.63338087262755016203262119192353, 0.0 }, - ZAxis = { 0.0, 0.0, 1.0 } -} - -asset.export("CarringtonLongitudeToHEEQ180Rotation", CarringtonLongitudeToHEEQ180Rotation) - - - -asset.meta = { - Name = "Carrington Longitude To HEEQ180 Rotation", - Description = "Contains a rotation for HEEQ180 to be used by another file", - Author = "OpenSpace Team", - URL = "http://openspaceproject.com", - License = "MIT license" -} diff --git a/data/assets/scene/solarsystem/heliosphere/bastille_day/fieldlines.asset b/data/assets/scene/solarsystem/heliosphere/bastille_day/fieldlines.asset index 402f770e1b..729e4360ac 100644 --- a/data/assets/scene/solarsystem/heliosphere/bastille_day/fieldlines.asset +++ b/data/assets/scene/solarsystem/heliosphere/bastille_day/fieldlines.asset @@ -1,5 +1,4 @@ -local heliosphereTransforms = asset.require("scene/solarsystem/sun/transforms_heliosphere") -local rot = asset.require("./carrington_to_heeq_rotation") +local sunTransforms = asset.require("scene/solarsystem/sun/transforms") @@ -16,12 +15,11 @@ local SunRadius = 695700000 -- Fieldlies from binaries local Fieldlines = { Identifier = "MAS-MHD-Fieldlines-bastille-day-2000", - Parent = heliosphereTransforms.HeliocentricEarthEquatorial180.Identifier, + Parent = sunTransforms.SunIAU.Identifier, -- TODO Elon: 21 April 2022. Interaction sphere should not depend on the transform scale. -- InteractionSphere = sunAsset.Sun.Renderable.Radii[1] * 1.05, InteractionSphere = 1 / SunRadius, Transform = { - Rotation = rot.CarringtonLongitudeToHEEQ180Rotation, Scale = { Type = "StaticScale", Scale = SunRadius @@ -30,17 +28,14 @@ local Fieldlines = { Renderable = { Type = "RenderableFieldlinesSequence", SourceFolder = fieldlinesDirectory, - AlphaBlendlingEnabled = false, InputFileType = "Osfls", + LineWidth = 1.0, + ShowAtAllTimes = false, + ColorQuantity = 0, ColorTablePaths = { - asset.resource("transferfunctions/density-fieldlines.txt"), - asset.resource("transferfunctions/velocity-fieldlines.txt") + asset.resource("transferfunctions/density-fieldlines.txt") }, - ColorTableRanges = { - { 0, 1000000 }, - { 100, 2000 } - }, - SimulationModel = "mas" + ColorMinMaxRange = { 0, 1000000 } }, GUI = { Path = "/Solar System/Heliosphere/Bastille Day 2000", diff --git a/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodes.asset b/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodes.asset index bfa8853ccd..17c1cca52d 100644 --- a/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodes.asset +++ b/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodes.asset @@ -1,5 +1,4 @@ -local heliosphereTransforms = asset.require("scene/solarsystem/sun/transforms_heliosphere") -local rot = asset.require("./carrington_to_heeq_rotation") +local sunTransforms = asset.require("scene/solarsystem/sun/transforms") @@ -14,13 +13,10 @@ local fluxNodesBinaries = asset.resource({ -- FluxNodes from binaries local Fluxnodes = { Identifier = "MAS-MHD-FluxNodes-bastille-day-2000", - Parent = heliosphereTransforms.HeliocentricEarthEquatorial180.Identifier, + Parent = sunTransforms.SunIAU.Identifier, -- TODO Elon: 21 April 2022. Interaction sphere should not depend on the transform scale. -- InteractionSphere = sunAsset.Sun.Renderable.Radii[1] * 1.05, InteractionSphere = 695700000.0, - Transform = { - Rotation = rot.CarringtonLongitudeToHEEQ180Rotation - }, Renderable = { Type = "RenderableFluxNodes", SourceFolder = fluxNodesBinaries, diff --git a/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodescutplane.asset b/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodescutplane.asset index 4d0bf11e2d..e213f3306f 100644 --- a/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodescutplane.asset +++ b/data/assets/scene/solarsystem/heliosphere/bastille_day/fluxnodescutplane.asset @@ -1,5 +1,5 @@ -local transforms = asset.require("scene/solarsystem/sun/transforms_heliosphere") -local rot = asset.require("./carrington_to_heeq_rotation") +local transforms = asset.require("scene/solarsystem/sun/transforms") +local transformsHelio = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") @@ -20,13 +20,10 @@ local TexturesPathMeridial = asset.resource({ local EquatorialCutplane = { Identifier = "EquatorialCutplane-bastille-day-2000", - Parent = transforms.HeliocentricEarthEquatorial180.Identifier, + Parent = transforms.SunIAU.Identifier, -- TODO Elon: 21 April 2022. Interaction sphere should not depend on the transform scale. -- InteractionSphere = sunAsset.Sun.Renderable.Radii[1] * 1.05, InteractionSphere = 695700000.0, - Transform = { - Rotation = rot.CarringtonLongitudeToHEEQ180Rotation - }, Renderable = { Type = "RenderablePlaneTimeVaryingImage", Size = 157000000000, @@ -46,7 +43,7 @@ local EquatorialCutplane = { local MeridialCutplane = { Identifier = "MeridialCutplane-bastille-day-2000", - Parent = transforms.HeliocentricEarthEquatorial180.Identifier, + Parent = transformsHelio.HeliocentricEarthEquatorial180.Identifier, -- TODO Elon: 21 April 2022. Interaction sphere should not depend on the transform scale. -- InteractionSphere = sunAsset.Sun.Renderable.Radii[1] * 1.05, InteractionSphere = 695700000, diff --git a/data/assets/scene/solarsystem/heliosphere/todayssun/fieldlines.asset b/data/assets/scene/solarsystem/heliosphere/todayssun/fieldlines.asset new file mode 100644 index 0000000000..bfd6792633 --- /dev/null +++ b/data/assets/scene/solarsystem/heliosphere/todayssun/fieldlines.asset @@ -0,0 +1,203 @@ +local transforms = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") + + + +local transferFunctions = asset.resource({ + Type = "HttpSynchronization", + Name = "Today's Sun Transfer Functions", + Identifier = "todayssun_transferfunctions", + Version = 1 +}) + + +local windSpeedPolarityColorTable = transferFunctions .. "polarity_spec.txt" +local subEarthLevelColorTable = transferFunctions .. "subearth_spec.txt" +local currentSheetColorTable = transferFunctions .. "currentsheet_spec.txt" +local opennessColorTable = transferFunctions .."openness_spec.txt" + +local infoURL = "https://iswaa-webservice1.ccmc.gsfc.nasa.gov/IswaSystemWebApp/DataInfoServlet?id=" +local dataURL = "https://iswaa-webservice1.ccmc.gsfc.nasa.gov/IswaSystemWebApp/FilesInRangeServlet?dataID=" + +local sunRadius = 695700000.0 + +local fieldlinesSCS = { + Identifier = "WSA_54_Fieldlines_SCS_OI", + Parent = transforms.WSAOffset60.Identifier, + Transform = { + Scale = { + Type = "StaticScale", + Scale = sunRadius + }, + }, + Renderable = { + Type = "RenderableFieldlinesSequence", + InputFileType = "Osfls", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2286, + ShowAtAllTimes = false, + ColorMethod = "By Quantity", + ColorQuantity = 0, -- Polarity & solar wind speed + ColorTablePaths = { + windSpeedPolarityColorTable, + currentSheetColorTable + }, + ColorTableRanges = { + { -1.0, 1.0 }, + { 0.0, 1.0 } + } + }, + GUI = { + Name = "Fieldlines: Corona SCS (Out-In Tracing)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[WSA 5.4 real-time output of the fieldline trace from the Schatten + Current Sheet model (SCS) outer boundary at 21.5 Rs to the source surface at 2.5 Rs + using GONGZ as input. SCS is the , a part of WSA.]], + Focusable = false + } +} + +local fieldlinesOI = { + Identifier = "WSA_54_Fieldlines_PFSS_OI", + Parent = transforms.WSAOffset60.Identifier, + Transform = { + Scale = { + Type = "StaticScale", + Scale = sunRadius + }, + }, + Renderable = { + Type = "RenderableFieldlinesSequence", + InputFileType = "Osfls", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2285, + ShowAtAllTimes = false, + ColorMethod = "By Quantity", + ColorQuantity = 0, -- Polarity & solar wind speed + ColorTablePaths = { + windSpeedPolarityColorTable, + currentSheetColorTable + }, + ColorTableRanges = { + { -1.0, 1.0 }, + { 0.0, 1.0 } + }, + }, + GUI = { + Name = "Fieldlines: Corona PFSS (Out-In Tracing)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[WSA 5.4 real-time output of the fieldline trace from the source + surface to the solar surface using GONGZ as input. PFSS is the Potential Field + Source Surface model, a part of WSA.]], + Focusable = false + } +} + +local fieldlinesIO = { + Identifier = "WSA_54_Fieldlines_PFSS_IO", + Parent = transforms.WSAOffset60.Identifier, + Transform = { + Scale = { + Type = "StaticScale", + Scale = sunRadius + }, + }, + Renderable = { + Type = "RenderableFieldlinesSequence", + InputFileType = "Osfls", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2284, + ShowAtAllTimes = false, + ColorMethod = "By Quantity", + ColorQuantity = 0, -- Open/closed lines + ColorTablePaths = { + opennessColorTable + }, + ColorTableRanges = { + { 0.0, 2.0 } + }, + }, + GUI = { + Name = "Fieldlines: Corona PFSS (In-Out Tracing)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[WSA 5.4 real-time output of the fieldline trace from the solar surface + outwards using GONGZ as input. PFSS is the Potential Field Source Surface model, a + part of WSA.]], + Focusable = false + } +} + +local fieldlinesEarth = { + Identifier = "WSA_54_Fieldlines_Earth", + Parent = transforms.WSAOffset60.Identifier, + Transform = { + Scale = { + Type = "StaticScale", + Scale = sunRadius + }, + }, + Renderable = { + Type = "RenderableFieldlinesSequence", + InputFileType = "Osfls", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2287, + ShowAtAllTimes = false, + ColorMethod = "By Quantity", + ColorQuantity = 0, -- Polarity + ColorTablePaths = { + windSpeedPolarityColorTable, + subEarthLevelColorTable + }, + ColorTableRanges = { + { 0.0, 1.0 }, + { 0.0, 2.0 } + }, + }, + GUI = { + Name = "Fieldlines: Tracing from Earth", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[WSA 5.4 real-time output of the field line trace from Earth using GONGZ + as input.]], + Focusable = false + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(fieldlinesSCS) + openspace.addSceneGraphNode(fieldlinesOI) + openspace.addSceneGraphNode(fieldlinesIO) + openspace.addSceneGraphNode(fieldlinesEarth) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(fieldlinesEarth) + openspace.removeSceneGraphNode(fieldlinesIO) + openspace.removeSceneGraphNode(fieldlinesOI) + openspace.removeSceneGraphNode(fieldlinesSCS) +end) + +asset.export(fieldlinesSCS) +asset.export(fieldlinesOI) +asset.export(fieldlinesIO) +asset.export(fieldlinesEarth) + + + +asset.meta = { + Name = "WSA 5.4. Streaming Field Line Data Dynamically", + Version = "1.0", + Description = [[Downloading data from the WSA 5.4 simulation model, showing the dynamic + Sun at any point. It includes .osfls files (OpenSpace FieldLine Sequence) for field + lines.]], + Author = "CCMC", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/scene/solarsystem/heliosphere/todayssun/grid.asset b/data/assets/scene/solarsystem/heliosphere/todayssun/grid.asset new file mode 100644 index 0000000000..b083d82a6b --- /dev/null +++ b/data/assets/scene/solarsystem/heliosphere/todayssun/grid.asset @@ -0,0 +1,128 @@ +local transforms = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") +local sunTransforms = asset.require("scene/solarsystem/sun/transforms") + + + +-- Slightly bigger size than the sphere radius to allow grid lines to not clip into sphere +-- since the lines themselfs are not curved +local gridSizeRadius = 7.0E8 + +local CarringtonPrimeMeridian = { + Identifier = "CarringtonPrimeMeridian", + Parent = sunTransforms.SunIAU.Identifier, + Transform = { + Translation = { + Type = "StaticTranslation", + -- A shift to only show an arc on one side of the sun + Position = { 3500000.0, 0.0, 0.0 } + }, + Rotation = { + Type = "StaticRotation", + Rotation = { 0.0, 0.0, -math.rad(90) } + }, + Scale = { + Type = "StaticScale", + -- Slightly smaller than grid size, to make the arc look better together with the + -- translation in x + Scale = 6.98E8 + } + }, + Renderable = { + Type = "RenderableSphericalGrid", + Size = gridSizeRadius, + Color = { 1.0, 0.0, 0.0 }, + LineWidth = 2.0, + LongSegments = 2, + LatSegments = 64 + }, + GUI = { + Name = "Carrington Prime Meridian", + Path = "/Solar System/Heliosphere", + Description = [[An arc showing the Carrington prime meridian of the Sun. + The line from pole to pole at 0 degree longitude.]], + Focusable = false + } +} + +local WSA_GridSlice = { + Identifier = "WSA_GridSlice", + Parent = transforms.HeliocentricEarthEquatorial.Identifier, + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = { 0.0, 0.0, -math.rad(90) } + }, + Scale = { + Type = "StaticScale", + Scale = gridSizeRadius + } + }, + Renderable = { + Type = "RenderableSphericalGrid", + Size = gridSizeRadius, + Color = { 0.8, 0.8, 0.8 }, + LineWidth = 2.0, + LongSegments = 2, + LatSegments = 64 + }, + GUI = { + Name = "Solar Longitude Facing the Earth", + Path = "/Solar System/Heliosphere", + Description = [[An arc on the Sun surface from pole to pole that always faces Earth.]], + Focusable = false + } +} + +local WSA_Grid10Degrees = { + Identifier = "WSA_Grid10Degrees", + Parent = transforms.HeliocentricEarthEquatorial.Identifier, + Transform = { + Scale = { + Type = "StaticScale", + Scale = gridSizeRadius + } + }, + Renderable = { + Type = "RenderableSphericalGrid", + Size = gridSizeRadius, + Color = { 0.035, 0.675, 0.255 }, + LineWidth = 1.0, + Segments = 36 + }, + GUI = { + Name = "Grid on Sun", + Path = "/Solar System/Heliosphere", + Description = [[A grid aligned with the Sun-Earth line, with a 10-degree + separation between line segments by default.]], + Focusable = false + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(CarringtonPrimeMeridian) + openspace.addSceneGraphNode(WSA_GridSlice) + openspace.addSceneGraphNode(WSA_Grid10Degrees) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(WSA_Grid10Degrees) + openspace.removeSceneGraphNode(WSA_GridSlice) + openspace.removeSceneGraphNode(CarringtonPrimeMeridian) +end) + +asset.export(CarringtonPrimeMeridian) +asset.export(WSA_GridSlice) +asset.export(WSA_Grid10Degrees) + + + +asset.meta = { + Name = "Real-time Sun Grid Lines", + Version = "1.0", + Description = [[Grids that help show the Sun's orientation and rotation in relation to + Earth, including the Carrington prime meridian and Earth-facing longitudes.]], + Author = "CCMC", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/scene/solarsystem/heliosphere/todayssun/mission.asset b/data/assets/scene/solarsystem/heliosphere/todayssun/mission.asset new file mode 100644 index 0000000000..67f4e67eb6 --- /dev/null +++ b/data/assets/scene/solarsystem/heliosphere/todayssun/mission.asset @@ -0,0 +1,65 @@ +local timeNow = openspace.time.currentWallTime() +local Mission = { + Identifier = "WSA", + Name = "The WSA Simulation Model", + TimeRange = { Start = "2022 DEC 09 16:14:00", End = timeNow }, + Description = [[ + This profile shows the Sun and its magnetic polarities, using data from the WSA + (Wang-Sheeley-Arge) simulation model version 5.4. The solar coronal portion of WSA is + comprised of two potential field type models. The inner model is Potential Field + Source Surface (PFSS) which specifies the coronal field from the inner, photospheric + boundary at 1 solar radii (Rs) to its outer boundary or source surface at 2.5Rs. The + outer model is the Schatten Current Sheet (SCS) model. The radial magnetic field + components of the PFSS magnetic field solution at 2.5Rs are used as the inner boundary + condition to the SCS model. All data is downloaded dynamically, bringing you the + latest data from the model. You can find these assets in the Scene menu under + "Solar System/Heliosphere". Under the "WSA Coronal Model" submenu, you will find extra + options for the solar field lines and surface features. The four different sets of + field lines are all following the magnetic fields but traced from different locations. + They can be colored by their different parameters in the data, for example polarity or + open or closed field lines. The two elements named "Solar Surface" show the magnetic + polarities, but have other options in the settings under "Texture Layer Options". The + same goes for the two named "Velocity at Outer Boundary". However, the two named + "Magnetic Field at 5Rs" do not have other options. Each asset includes settings for + appearance. If field lines are looking too bright against a bright background, try + changing the setting for additive blending. Additionally there is an option to save + the downloaded data for future use (by default, the data is not cached). Each data + file remains active until the next one in the sequence becomes available. To see which + file is currently being used, check the "Texture Source" field for each asset in the + GUI. In the visualization, the white line shooting out from the Sun, along with the + white arc, always points in the direction of Earth. The red arc is the Carrington + prime meridian which is longitude 0 of the Sun, and the green grid shows lines with a + 10-degree separation. If no data is showing, most likely the selected time is outside + of the data range (e.g., too far in the future or the past for the dynamic data to be + available). Alternatively, the files failed to download. For more information on the + simulation model, see https://ccmc.gsfc.nasa.gov/models/WSA~5.4. + ]], + Milestones = { + { + Name = "Version 5.4", + Date = "2022 DEC 09 16:14:00", + Description = [[For version 5.4 of WSA fieldline data and solar surfaces from end of + 2022 until today.]] + } + } +} + + +asset.onInitialize(function() + openspace.loadMission(Mission) +end) + +asset.onDeinitialize(function() + openspace.unloadMission(Mission) +end) + + + +asset.meta = { + Name = "Overview information panel - Mission panel", + Description = [[This mission file provides information about the simulation model + WSA.]], + Author = "CCMC", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/scene/solarsystem/heliosphere/todayssun/sun_earth_line.asset b/data/assets/scene/solarsystem/heliosphere/todayssun/sun_earth_line.asset new file mode 100644 index 0000000000..d136f57209 --- /dev/null +++ b/data/assets/scene/solarsystem/heliosphere/todayssun/sun_earth_line.asset @@ -0,0 +1,43 @@ +local earth = asset.require("scene/solarsystem/planets/earth/earth") +local sun = asset.require("scene/solarsystem/sun/sun") + + + +local SunEarthLine = { + Identifier = "RenderableNodeLine_Sun_Earth", + Renderable = { + Type = "RenderableNodeLine", + StartNode = sun.Sun.Identifier, + EndNode = earth.Earth.Identifier + }, + GUI = { + Name = "Sun-Earth Line", + Path = "/Solar System/Heliosphere", + Description = [[A line between the Sun and the Earth to help with spatial + orientation.]], + Focusable = false + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(SunEarthLine) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(SunEarthLine) +end) + +asset.export("NodeLine", SunEarthLine) + + + +asset.meta = { + Name = "Sun-Earth Line", + Version = "1.0", + Description = [[A line between the Sun and the Earth to help with spatial + orientation.]], + Author = "CCMC", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/scene/solarsystem/heliosphere/todayssun/surfaces.asset b/data/assets/scene/solarsystem/heliosphere/todayssun/surfaces.asset new file mode 100644 index 0000000000..619f50d616 --- /dev/null +++ b/data/assets/scene/solarsystem/heliosphere/todayssun/surfaces.asset @@ -0,0 +1,283 @@ +local transforms = asset.require("scene/solarsystem/heliosphere/transforms_heliosphere") + + + +local transferFunctions = asset.resource({ + Type = "HttpSynchronization", + Name = "Today's Sun Transfer Functions", + Identifier = "todayssun_transferfunctions", + Version = 1 +}) + + +local blueBlackRed = transferFunctions .. "blue-black-red.txt" + +local infoURL = "https://iswaa-webservice1.ccmc.gsfc.nasa.gov/IswaSystemWebApp/DataInfoServlet?id=" +local dataURL = "https://iswaa-webservice1.ccmc.gsfc.nasa.gov/IswaSystemWebApp/FilesInRangeServlet?dataID=" + +local sunRadius = 695700000 +-- This small 0.2% increase is an arbitrary number but necessary to make sure it is not +-- within the Sun sphere +local extendedRadius = sunRadius * 1.002 +local fiveSunRadius = sunRadius * 5 +local outerBoundary = sunRadius * 21.5 +-- Slightly bigger radii to prevent z fighting rendering artifact compared to GONG-Z +local extendedRadiusAdapt = extendedRadius * 1.00002 +local fiveSunRadiusAdapt = fiveSunRadius * 1.00002 +local outerBoundaryAdapt = outerBoundary * 1.00002 + +local WSA_54_Magnetic_Field_GONGZ_5_Rs = { + Identifier = "WSA_54_Magnetic_Field_GONGZ_5_Rs", + Parent = transforms.WSAOffset60.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = fiveSunRadius, + Orientation = "Both", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2148, + FitsLayer = 0, + LayerNames = { + ['0'] = "Coronal Magnetic Field at 5 Solar Radii (nT)" + }, + LayerMinMaxCapValues = { + ['0'] = { -5000.0, 5000.0 } + }, + ColorMap = blueBlackRed, + UseColorMap = false, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Magnetic Field at 5Rs (GONG-Z)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing the coronal + magnetic field on a sphere at 5 solar radii. Output: FITS files using GONG-Z Maps + (RADOUT = 5.0).]], + Focusable = false + } +} + +local WSA_54_Magnetic_Field_ADAPT_5_Rs = { + Identifier = "WSA_54_Magnetic_Field_ADAPT_5_Rs", + Parent = transforms.HeliocentricEarthEquatorial180.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = fiveSunRadiusAdapt, + Orientation = "Both", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2149, + FitsLayer = 0, + LayerNames = { + ['0'] = "Coronal Magnetic Field at 5 Solar Radii (nT)" + }, + LayerMinMaxCapValues = { + ['0'] = { -5000.0, 5000.0 } + }, + ColorMap = blueBlackRed, + UseColorMap = false, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Magnetic Field at 5Rs (GONG ADAPT)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing the coronal + magnetic field on a sphere at 5 solar radii. Output: FITS file using + ADAPT GONG realization 000 to 011 Maps (RADOUT = 5.0).]], + Focusable = false + } +} + +local WSA_54_Magnetograms_GONGZ = { + Identifier = "WSA_54_Magnetograms_GONGZ", + Parent = transforms.WSAOffset60.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = extendedRadius, + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2148, + FitsLayer = 4, + LayerNames = { + ['1'] = "Flux tube expansion factor evaluated at the source surface", + ['4'] = "Observed Photospheric Field (Gauss)", + ['5'] = "Distance from open field footpoint to nearest coronal boundary (deg)", + ['6'] = "Open (1,2,3) and closed (0) regions on the photosphere [1=in-to-out tracing; 2=out-to-in tracing; 3=both] (no units)", + ['7'] = "Distance to current sheet at outer boundary (degrees)" + }, + LayerMinMaxCapValues = { + ['1'] = { 0.0, 50.0 }, + ['4'] = { -50.0, 50.0 }, + ['5'] = { 0.0, 30.0 }, + ['6'] = { 0.0, 4.0 }, + ['7'] = { 0.0, 90.0 } + }, + ColorMap = blueBlackRed, + UseColorMap = false, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Solar Surface (GONG-Z)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing data on the + solar surface with multiple options in the list under Texture Layer Options. Output: + FITS files using GONG-Z Maps (RADOUT = 5.0).]], + Focusable = false + } +} + +local WSA_54_Magnetograms_ADAPT = { + Identifier = "WSA_54_Magnetograms_ADAPT", + Parent = transforms.HeliocentricEarthEquatorial180.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = extendedRadiusAdapt, + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2149, + FitsLayer = 4, + LayerNames = { + ['1'] = "Flux tube expansion factor evaluated at the source surface", + ['4'] = "Observed Photospheric Field (Gauss)", + ['5'] = "Distance from open field footpoint to nearest coronal boundary (deg)", + ['6'] = "Open (1,2,3) and closed (0) regions on the photosphere [1=in-to-out tracing; 2=out-to-in tracing; 3=both] (no units)", + ['7'] = "Distance to current sheet at outer boundary (degrees)" + }, + LayerMinMaxCapValues = { + ['1'] = { 0.0, 50.0 }, + ['4'] = { -50.0, 50.0 }, + ['5'] = { 0.0, 30.0 }, + ['6'] = { 0.0, 4.0 }, + ['7'] = { 0.0, 90.0 } + }, + ColorMap = blueBlackRed, + UseColorMap = false, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Solar Surface (GONG ADAPT)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing data on the + solar surface with multiple options in the list under Texture Layer Options. Output: + FITS file using ADAPT GONG realization 000 to 011 Maps (RADOUT = 5.0).]], + Focusable = false + } +} + +local WSA_Velocity_Adapt_Outer_Boundary = { + Identifier = "WSA_54_velocity_ADAPT_Outer_Boundary", + Parent = transforms.HeliocentricEarthEquatorial180.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = outerBoundaryAdapt, + LoadingType = "DynamicDownloading", + Orientation = "Both", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2218, + FitsLayer = 1, + LayerNames = { + ['0'] = "Coronal Magnetic Field at 21.5 Solar Radii (nT)", + ['1'] = "Solar Wind Speed at 21.5 Solar Radii (km/s)" + }, + LayerMinMaxCapValues = { + ['0'] = { -200.0, 200.0 }, + ['1'] = { 200.0, 850.0 } + }, + ColorMap = blueBlackRed, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Velocity at Outer Boundary (GONG-ADAPT)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing data on a sphere + at the outer boundary of the simulation model, either solar wind speed or coronal + magnetic field. Output: FITS file using ADAPT GONG realization 000-011 Maps + (RADOUT = 21.5).]], + Focusable = false + } +} + +local WSA_Velocity_Gongz_Outer_Boundary = { + Identifier = "WSA_54_velocity_GONGZ_Outer_Boundary", + Parent = transforms.WSAOffset60.Identifier, + Renderable = { + Type = "RenderableTimeVaryingFitsSphere", + Size = outerBoundary, + Orientation = "Both", + LoadingType = "DynamicDownloading", + InfoURL = infoURL, + DataURL = dataURL, + DataID = 2217, + FitsLayer = 1, + LayerNames = { + ['0'] = "Coronal Magnetic Field at 21.5 Solar Radii (nT)", + ['1'] = "Solar Wind Speed at 21.5 Solar Radii (km/s)" + }, + LayerMinMaxCapValues = { + ['0'] = { -200.0, 200.0 }, + ['1'] = { 200.0, 850.0 } + }, + ColorMap = blueBlackRed, + ShowPastFirstAndLastFile = false, + Segments = 132 + }, + GUI = { + Name = "Velocity at Outer Boundary (GONG-Z)", + Path = "/Solar System/Heliosphere/WSA Coronal Model", + Description = [[Texture sequence of simulation model WSA 5.4, showing data on a sphere + at the outer boundary of the simulation model, either solar wind speed or coronal + magnetic field. Output: FITS file using GONG-Z Maps (RADOUT = 21.5).]], + Focusable = false + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(WSA_54_Magnetic_Field_GONGZ_5_Rs) + openspace.addSceneGraphNode(WSA_54_Magnetic_Field_ADAPT_5_Rs) + openspace.addSceneGraphNode(WSA_54_Magnetograms_GONGZ) + openspace.addSceneGraphNode(WSA_54_Magnetograms_ADAPT) + openspace.addSceneGraphNode(WSA_Velocity_Adapt_Outer_Boundary) + openspace.addSceneGraphNode(WSA_Velocity_Gongz_Outer_Boundary) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(WSA_Velocity_Gongz_Outer_Boundary) + openspace.removeSceneGraphNode(WSA_Velocity_Adapt_Outer_Boundary) + openspace.removeSceneGraphNode(WSA_54_Magnetograms_ADAPT) + openspace.removeSceneGraphNode(WSA_54_Magnetograms_GONGZ) + openspace.removeSceneGraphNode(WSA_54_Magnetic_Field_ADAPT_5_Rs) + openspace.removeSceneGraphNode(WSA_54_Magnetic_Field_GONGZ_5_Rs) +end) + +asset.export(WSA_54_Magnetic_Field_GONGZ_5_Rs) +asset.export(WSA_54_Magnetic_Field_ADAPT_5_Rs) +asset.export(WSA_54_Magnetograms_GONGZ) +asset.export(WSA_54_Magnetograms_ADAPT) +asset.export(WSA_Velocity_Adapt_Outer_Boundary) +asset.export(WSA_Velocity_Gongz_Outer_Boundary) + + + +asset.meta = { + Name = "WSA 5.4. Streaming Surface Data Dynamically", + Version = "1.0", + Description = [[Downloading data from the WSA 5.4 simulation model, showing the dynamic + Sun at any point. It includes .fits files for solar surface data. GONG-Z is the + zero-corrected synoptic surface magnetic maps from the Global Oscillation Network + Group, while GONG-ADAPT is a model based on GONG, where ADAPT stands for Air Force + Data Assimilative Photospheric Flux Transport.]], + Author = "CCMC", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/scene/solarsystem/sun/transforms_heliosphere.asset b/data/assets/scene/solarsystem/heliosphere/transforms_heliosphere.asset similarity index 86% rename from data/assets/scene/solarsystem/sun/transforms_heliosphere.asset rename to data/assets/scene/solarsystem/heliosphere/transforms_heliosphere.asset index 232cb0bb8a..c7ac379bf0 100644 --- a/data/assets/scene/solarsystem/sun/transforms_heliosphere.asset +++ b/data/assets/scene/solarsystem/heliosphere/transforms_heliosphere.asset @@ -101,6 +101,24 @@ local HeliocentricEarthEcliptic = { } } +local WSAOffset60 = { + Identifier = "WSAOffset60", + Parent = HeliocentricEarthEquatorial.Identifier, + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = { 0, 0, -math.rad(60) } + } + }, + GUI = { + Name = "WSA Data Offset 60 Degrees", + Path = "/Solar System/Sun", + Description = [[WSA outputs (FITS files) have the Sun-Earth line 60 degrees shifted + from the edge. This scene graph node handles that transformation.]], + Hidden = true + } +} + asset.onInitialize(function() openspace.spice.loadKernel(SunCentricFrameKernels .. "HEEQ180.tf") @@ -109,9 +127,11 @@ asset.onInitialize(function() openspace.addSceneGraphNode(HeliocentricEarthEquatorial180) openspace.addSceneGraphNode(HeliocentricEarthEquatorial) openspace.addSceneGraphNode(HeliocentricEarthEcliptic) + openspace.addSceneGraphNode(WSAOffset60) end) asset.onDeinitialize(function() + openspace.removeSceneGraphNode(WSAOffset60) openspace.removeSceneGraphNode(HeliocentricEarthEcliptic) openspace.removeSceneGraphNode(HeliocentricEarthEquatorial) openspace.removeSceneGraphNode(HeliocentricEarthEquatorial180) @@ -123,12 +143,14 @@ end) asset.export(HeliocentricEarthEquatorial180) asset.export(HeliocentricEarthEquatorial) asset.export(HeliocentricEarthEcliptic) +asset.export(WSAOffset60) asset.meta = { Name = "Sun Transform, HEE, HEEQ and HEEQ180", - Description = "Sun transform: HEE, HEEQ and HEEQ180", + Description = [[Sun transform: HEE, HEEQ, HEEQ180 and a 60-degree fixed rotation that WSA + is using.]], Author = "CCMC", URL = "http://openspaceproject.com", License = "MIT license" diff --git a/data/assets/scene/solarsystem/missions/juice/fieldlines.asset b/data/assets/scene/solarsystem/missions/juice/fieldlines.asset index e578bd3471..080868dc08 100644 --- a/data/assets/scene/solarsystem/missions/juice/fieldlines.asset +++ b/data/assets/scene/solarsystem/missions/juice/fieldlines.asset @@ -18,11 +18,13 @@ local GanymedeMagnetosphere = { SourceFolder = data, LineWidth = 3.0, InputFileType = "Json", + ShowAtAllTimes = true, ColorMethod = "By Quantity", ColorQuantity = 0, - ColorTableRanges = { { 62.556353386366766, 1665.5534182835445 } }, + ColorTableRanges = { { 62.556353386366766, 1665.5534182835445} }, + ColorMinMaxRange = { 0, 10000 }, ColorTablePaths = { asset.resource("CMR-illuminance2.txt") }, - Color = { 0.03, 0.0, 0.0, 1.0 }, + Color = { 1.0, 0.725, 0.75, 0.8 }, ParticleSpacing = 42.0, ParticleSize = 30.0, FlowColor = { 1.0, 1.0, 1.0, 0.1 }, diff --git a/data/assets/scene/solarsystem/sun/EUV_layer.asset b/data/assets/scene/solarsystem/sun/euv_layer.asset similarity index 98% rename from data/assets/scene/solarsystem/sun/EUV_layer.asset rename to data/assets/scene/solarsystem/sun/euv_layer.asset index 755a756111..65e7820ab0 100644 --- a/data/assets/scene/solarsystem/sun/EUV_layer.asset +++ b/data/assets/scene/solarsystem/sun/euv_layer.asset @@ -33,7 +33,7 @@ local EUVLayer = { } local ToggleEuv = { - Identifier = "os.solarsystem.ToggleEuv", + Identifier = "os.solarsystem.sun.ToggleEuv", Name = "Toggle EUV layer", Command = [[ if openspace.propertyValue("Scene.EUV-Layer-bastille-day-2000.Renderable.Enabled") then diff --git a/data/profiles/spaceweather/bastilleday2000.profile b/data/profiles/spaceweather/bastilleday2000.profile index c7fcb209d0..9a0416504c 100644 --- a/data/profiles/spaceweather/bastilleday2000.profile +++ b/data/profiles/spaceweather/bastilleday2000.profile @@ -79,7 +79,7 @@ "key": "I" }, { - "action": "os.solarsystem.ToggleEuv", + "action": "os.solarsystem.sun.ToggleEuv", "key": "E" }, { diff --git a/data/profiles/spaceweather/todays_sun.profile b/data/profiles/spaceweather/todays_sun.profile new file mode 100644 index 0000000000..2a1f03812a --- /dev/null +++ b/data/profiles/spaceweather/todays_sun.profile @@ -0,0 +1,164 @@ +{ + "assets": [ + "base", + "base_keybindings", + "scene/solarsystem/heliosphere/todayssun/fieldlines", + "scene/solarsystem/heliosphere/todayssun/grid", + "scene/solarsystem/heliosphere/todayssun/mission", + "scene/solarsystem/heliosphere/todayssun/sun_earth_line", + "scene/solarsystem/heliosphere/todayssun/surfaces", + "scene/solarsystem/planets/earth/earth", + "scene/solarsystem/planets/earth/satellites/satellites" + ], + "camera": { + "altitude": 4578000000.0, + "anchor": "Sun", + "latitude": 1.5877, + "longitude": -150.1924, + "type": "goToGeo" + }, + "delta_times": [ + 1.0, + 5.0, + 30.0, + 60.0, + 300.0, + 1800.0, + 3600.0, + 43200.0, + 86400.0, + 604800.0, + 1209600.0, + 2592000.0, + 5184000.0, + 7776000.0, + 15552000.0, + 31536000.0, + 63072000.0, + 157680000.0, + 315360000.0, + 630720000.0 + ], + "keybindings": [ + { + "action": "os.solarsystem.ToggleSatelliteTrails", + "key": "S" + } + ], + "mark_nodes": [ + "Sun", + "Earth" + ], + "meta": { + "author": "OpenSpace Team", + "description": "This profile uses simulation outputs in real time from the model WSA to visualize the magnetic fluctuations on the Sun.", + "license": "MIT License", + "name": "Todays Sun", + "url": "https://www.openspaceproject.com", + "version": "1.0" + }, + "panel_visibility": { + "exoplanets": false, + "flightControl": false, + "geoLocation": false, + "gettingStartedTour": false, + "mission": true, + "skyBrowser": false + }, + "properties": [ + { + "name": "{earth_satellites}.Renderable.Enabled", + "type": "setPropertyValue", + "value": "false" + }, + { + "name": "Scene.Sun.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "true" + }, + { + "name": "Scene.SunGlare.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.Earth.Renderable.Layers.ColorLayers.Blue_Marble.Enabled", + "type": "setPropertyValueSingle", + "value": "true" + }, + { + "name": "Scene.Earth.Renderable.Layers.ColorLayers.ESRI_VIIRS_Combo.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_velocity_GONGZ_Outer_Boundary.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_velocity_ADAPT_Outer_Boundary.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Magnetograms_ADAPT.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Magnetograms_GONGZ.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "true" + }, + { + "name": "Scene.WSA_54_Magnetic_Field_ADAPT_5_Rs.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Magnetic_Field_GONGZ_5_Rs.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Fieldlines_PFSS_IO.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "true" + }, + { + "name": "Scene.WSA_54_Fieldlines_PFSS_OI.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "true" + }, + { + "name": "Scene.WSA_54_Fieldlines_SCS_OI.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Fieldlines_Earth.Renderable.Enabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Fieldlines_PFSS_IO.Renderable.ABlendingEnabled", + "type": "setPropertyValueSingle", + "value": "false" + }, + { + "name": "Scene.WSA_54_Fieldlines_PFSS_IO.Renderable.LineWidth", + "type": "setPropertyValueSingle", + "value": "2.0" + } + ], + "time": { + "is_paused": false, + "type": "relative", + "value": "-2h" + }, + "version": { + "major": 1, + "minor": 4 + } +} diff --git a/include/openspace/properties/propertyowner.h b/include/openspace/properties/propertyowner.h index 8c7e8761f8..1849f08a28 100644 --- a/include/openspace/properties/propertyowner.h +++ b/include/openspace/properties/propertyowner.h @@ -250,7 +250,7 @@ public: * PropertyOwner. This method will also inform the Property about the change in * ownership by calling the Property::setPropertyOwner method. * - * \param prop The Property whose ownership is changed. + * \param prop The Property whose ownership is changed */ void addProperty(Property* prop); diff --git a/include/openspace/util/dynamicfilesequencedownloader.h b/include/openspace/util/dynamicfilesequencedownloader.h new file mode 100644 index 0000000000..699c5a20d9 --- /dev/null +++ b/include/openspace/util/dynamicfilesequencedownloader.h @@ -0,0 +1,107 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_CORE___DYNAMICFILESEQUENCEDOWNLOADER___H__ +#define __OPENSPACE_CORE___DYNAMICFILESEQUENCEDOWNLOADER___H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace { + +struct File { + std::unique_ptr download; + std::string timestep; + double time = 0.0; + std::string URL; + std::filesystem::path path; + double cadence = 0.0; + int availableIndex = -1; + enum class State { + Available, + OnQueue, + Downloading, + Downloaded, + Unknown + }; + State state = State::Unknown; +}; + +class DynamicFileSequenceDownloader { +public: + DynamicFileSequenceDownloader(int dataID, const std::string& identifier, + std::string infoUrl, std::string dataUrl, size_t nFilesToQueue); + + void deinitialize(bool cacheFiles); + void requestDataInfo(std::string httpInfoRequest); + void requestAvailableFiles(std::string httpDataRequest, + std::filesystem::path syncDir); + std::vector::iterator closestFileToNow(double time); + void update(double time, double deltaTime); + const std::vector& downloadedFiles() const; + void checkForFinishedDownloads(); + void clearDownloaded(); + const std::filesystem::path& destinationDirectory() const; + bool areFilesCurrentlyDownloading() const; + const std::vector& filesCurrentlyDownloading() const; + +private: + void downloadFile(); + double calculateCadence() const; + void putInQueue(); + + bool _isForwardDirection = true; + bool _isFirstFrame = true; + bool _isSecondFrame = true; + bool _hasNotifiedTooFast = false; + + std::filesystem::path _syncDir; + std::filesystem::path _trackSynced; + const int _dataID; + const std::string _infoUrl; + const std::string _dataUrl; + + double _dataMinTime = 0.0; + double _dataMaxTime = 0.0; + + const size_t _nFilesToQueue = 0; + + std::vector::iterator _currentFile; + + std::vector _availableData; + std::vector _queuedFilesToDownload; + std::vector _filesCurrentlyDownloading; + std::vector _downloadedFiles; +}; + +} // namespace openspace + +#endif // __OPENSPACE_CORE___DYNAMICFILESEQUENCEDOWNLOADER___H__ diff --git a/modules/base/rendering/renderablesphere.cpp b/modules/base/rendering/renderablesphere.cpp index 187957b834..479246258b 100644 --- a/modules/base/rendering/renderablesphere.cpp +++ b/modules/base/rendering/renderablesphere.cpp @@ -40,6 +40,7 @@ #include namespace { + constexpr std::string_view _loggerCat = "RenderableSphere"; constexpr int DefaultBlending = 0; constexpr int AdditiveBlending = 1; constexpr int PolygonBlending = 2; @@ -112,6 +113,21 @@ namespace { openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo UseColorMapInfo = { + "UseColorMap", + "Use Color Map", + "Used to toggle color map on or off for the sphere. Mainly used to transform " + "grayscale textures from data into color images.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo ColorMapInfo = { + "ColorMap", + "Transfer Function (Color Map) Path", + "Color map / Transfer function to use if `UseColorMap` is enabled.", + openspace::properties::Property::Visibility::AdvancedUser + }; + constexpr openspace::properties::Property::PropertyInfo BlendingOptionInfo = { "BlendingOption", "Blending Options", @@ -161,6 +177,12 @@ namespace { // [[codegen::verbatim(FadeOutThresholdInfo.description)]] std::optional fadeOutThreshold [[codegen::inrange(0.0, 1.0)]]; + // [[codegen::verbatim(ColorMapInfo.description)]] + std::optional colorMap; + + // [[codegen::verbatim(UseColorMapInfo.description)]] + std::optional useColorMap; + // [[codegen::verbatim(BlendingOptionInfo.description)]] std::optional blendingOption; @@ -187,6 +209,8 @@ RenderableSphere::RenderableSphere(const ghoul::Dictionary& dictionary) , _fadeOutThreshold(FadeOutThresholdInfo, 0.f, 0.f, 1.f, 0.001f) , _blendingFuncOption(BlendingOptionInfo) , _disableDepth(DisableDepthInfo, false) + , _useColorMap(UseColorMapInfo, false) + , _colorMap(ColorMapInfo) { const Parameters p = codegen::bake(dictionary); @@ -244,6 +268,33 @@ RenderableSphere::RenderableSphere(const ghoul::Dictionary& dictionary) addProperty(_disableDepth); setBoundingSphere(_size); + + if (p.colorMap.has_value()) { + _colorMap = p.colorMap->string(); + _useColorMap = true; + } + addProperty(_useColorMap); + + _colorMap.onChange([this]() { + if (!std::filesystem::exists(_colorMap.value())) { + LERROR(std::format( + "Path {} to color map is invalid.", + _colorMap.value() + )); + return; + } + _transferFunction = std::make_unique(_colorMap.value()); + }); + addProperty(_colorMap); + + // This check is after color map in case a color map is given + // but using it on start-up is set to false. + if (p.useColorMap.has_value()) { + if (!p.colorMap.has_value()) { + throw ghoul::RuntimeError("No color map path was provided"); + } + _useColorMap = *p.useColorMap; + } } bool RenderableSphere::isReady() const { @@ -266,6 +317,10 @@ void RenderableSphere::initializeGL() { ); ghoul::opengl::updateUniformLocations(*_shader, _uniformCache); + + if (_useColorMap) { + _transferFunction = std::make_unique(_colorMap.value()); + } } void RenderableSphere::deinitializeGL() { @@ -278,6 +333,7 @@ void RenderableSphere::deinitializeGL() { } ); _shader = nullptr; + _transferFunction = nullptr; } void RenderableSphere::render(const RenderData& data, RendererTasks&) { @@ -364,6 +420,16 @@ void RenderableSphere::render(const RenderData& data, RendererTasks&) { return; } + // TextureUnit cannot be declared in if statement below + ghoul::opengl::TextureUnit transferFunctionUnit; + _shader->setUniform("usingTransferFunction", _useColorMap); + _shader->setUniform("transferFunction", transferFunctionUnit); + _shader->setUniform("dataMinMaxValues", _dataMinMaxValues); + if (_useColorMap) { + transferFunctionUnit.activate(); + _transferFunction->bind(); + } + _shader->setUniform(_uniformCache.opacity, adjustedOpacity); _shader->setUniform(_uniformCache.mirrorTexture, _mirrorTexture.value()); @@ -430,7 +496,9 @@ void RenderableSphere::update(const UpdateData&) { _shader->rebuildFromFile(); ghoul::opengl::updateUniformLocations(*_shader, _uniformCache); } - + if (!_transferFunction && std::filesystem::exists(_colorMap.value())) { + _transferFunction = std::make_unique(_colorMap.value()); + } if (_sphereIsDirty) [[unlikely]] { _sphere = std::make_unique(_size, _segments); _sphere->initialize(); diff --git a/modules/base/rendering/renderablesphere.h b/modules/base/rendering/renderablesphere.h index cd5613831d..9a6c835607 100644 --- a/modules/base/rendering/renderablesphere.h +++ b/modules/base/rendering/renderablesphere.h @@ -28,6 +28,8 @@ #include #include +#include +#include #include namespace ghoul::opengl { class ProgramObject; } @@ -70,12 +72,17 @@ protected: properties::OptionProperty _blendingFuncOption; properties::BoolProperty _disableDepth; -private: + glm::vec2 _dataMinMaxValues; ghoul::opengl::ProgramObject* _shader = nullptr; + properties::BoolProperty _useColorMap; +private: std::unique_ptr _sphere; bool _sphereIsDirty = false; + properties::StringProperty _colorMap; + std::unique_ptr _transferFunction; + UniformCache(opacity, modelViewProjection, modelViewTransform, modelViewRotation, colorTexture, mirrorTexture) _uniformCache; }; diff --git a/modules/base/rendering/renderabletimevaryingsphere.cpp b/modules/base/rendering/renderabletimevaryingsphere.cpp index 6434769cf3..496909f558 100644 --- a/modules/base/rendering/renderabletimevaryingsphere.cpp +++ b/modules/base/rendering/renderabletimevaryingsphere.cpp @@ -68,7 +68,10 @@ namespace { namespace openspace { documentation::Documentation RenderableTimeVaryingSphere::Documentation() { - return codegen::doc("base_renderable_time_varying_sphere"); + return codegen::doc( + "base_renderable_time_varying_sphere", + RenderableSphere::Documentation() + ); } RenderableTimeVaryingSphere::RenderableTimeVaryingSphere( @@ -79,20 +82,15 @@ RenderableTimeVaryingSphere::RenderableTimeVaryingSphere( const Parameters p = codegen::bake(dictionary); _textureSourcePath = p.textureSource.string(); + extractMandatoryInfoFromSourceFolder(); + computeSequenceEndTime(); + loadTexture(); } bool RenderableTimeVaryingSphere::isReady() const { return RenderableSphere::isReady() && _texture; } -void RenderableTimeVaryingSphere::initializeGL() { - RenderableSphere::initializeGL(); - - extractMandatoryInfoFromSourceFolder(); - computeSequenceEndTime(); - loadTexture(); -} - void RenderableTimeVaryingSphere::deinitializeGL() { _texture = nullptr; _files.clear(); @@ -148,9 +146,14 @@ void RenderableTimeVaryingSphere::extractMandatoryInfoFromSourceFolder() { void RenderableTimeVaryingSphere::update(const UpdateData& data) { RenderableSphere::update(data); + if (_files.empty()) { + return; + } + const double currentTime = data.time.j2000Seconds(); const bool isInInterval = (currentTime >= _files[0].time) && (currentTime < _sequenceEndTime); + if (isInInterval) { const size_t nextIdx = _activeTriggerTimeIndex + 1; if ( diff --git a/modules/base/rendering/renderabletimevaryingsphere.h b/modules/base/rendering/renderabletimevaryingsphere.h index a6322d572b..5bb35a6d92 100644 --- a/modules/base/rendering/renderabletimevaryingsphere.h +++ b/modules/base/rendering/renderabletimevaryingsphere.h @@ -42,7 +42,6 @@ class RenderableTimeVaryingSphere : public RenderableSphere { public: explicit RenderableTimeVaryingSphere(const ghoul::Dictionary& dictionary); - void initializeGL() override; void deinitializeGL() override; bool isReady() const override; @@ -64,7 +63,6 @@ private: void extractMandatoryInfoFromSourceFolder(); void updateActiveTriggerTimeIndex(double currenttime); void computeSequenceEndTime(); - // If there's just one state it should never disappear! double _sequenceEndTime = std::numeric_limits::max(); std::vector _files; diff --git a/modules/base/shaders/sphere_fs.glsl b/modules/base/shaders/sphere_fs.glsl index 8800b47bc8..98ecb7ff62 100644 --- a/modules/base/shaders/sphere_fs.glsl +++ b/modules/base/shaders/sphere_fs.glsl @@ -30,6 +30,11 @@ in vec3 vs_normal; in float vs_screenSpaceDepth; uniform sampler2D colorTexture; + +uniform bool usingTransferFunction = false; +uniform sampler1D transferFunction; +uniform vec2 dataMinMaxValues; + uniform float opacity; uniform bool mirrorTexture; @@ -41,8 +46,20 @@ Fragment getFragment() { if (mirrorTexture) { texCoord.x = 1.0 - texCoord.x; } - - frag.color = texture(colorTexture, texCoord); + if (usingTransferFunction) { + vec4 dataValue = texture(colorTexture, texCoord); + float minVal = dataMinMaxValues.x; + float maxVal = dataMinMaxValues.y; + // dataValue and minVal comes from the same texture so dataValue cannot be < minVal + float lookUpVal = (dataValue.x - minVal) / (maxVal - minVal); + frag.color = vec4( + texture(transferFunction, lookUpVal).rgb, + 1.0 + ); + } + else { + frag.color = texture(colorTexture, texCoord); + } frag.color.a *= opacity; frag.depth = vs_screenSpaceDepth; diff --git a/modules/exoplanets/exoplanetsmodule_lua.inl b/modules/exoplanets/exoplanetsmodule_lua.inl index b2c18ad4cc..ddfd048314 100644 --- a/modules/exoplanets/exoplanetsmodule_lua.inl +++ b/modules/exoplanets/exoplanetsmodule_lua.inl @@ -169,7 +169,7 @@ std::vector hostStarsWithSufficientData() { * The data is retrieved from the module's prepared datafile for exoplanets. This file is * in a binary format, for fast retrieval during runtime. * - * \param starName The name of the star to get the information for. + * \param starName The name of the star to get the information for * * \return An object of the type [ExoplanetSystemData](#exoplanets_exoplanet_system_data) * that can be used to create the scene graph nodes for the exoplanet system @@ -191,7 +191,7 @@ std::vector hostStarsWithSufficientData() { /** * Remove a loaded exoplanet system. * - * \param starName The name of the host star for the system to remove. + * \param starName The name of the host star for the system to remove */ [[codegen::luawrap]] void removeExoplanetSystem(std::string starName) { using namespace openspace; @@ -248,7 +248,7 @@ std::vector hostStarsWithSufficientData() { * When dowloading the data from the archive we recommend including all columns, since a * few required ones are not selected by default. * - * \param csvFile A path to the CSV file to load the data from. + * \param csvFile A path to the CSV file to load the data from * * \return A list of objects of the type * [ExoplanetSystemData](#exoplanets_exoplanet_system_data), that can be used to diff --git a/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h b/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h index 4c5ae3feee..5291c46481 100644 --- a/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h +++ b/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h @@ -65,7 +65,7 @@ public: * * \param row The row to parse, given as a string * \param columnNames The list of column names in the file, from the CSV header - * \param positionSourceFile A SPECK file to use for getting the position of the star. + * \param positionSourceFile A SPECK file to use for getting the position of the star * This is used to make sure the position of the star matches those of other * star datasets. If no file is provided, the position from the CSV data file * is read and used instead diff --git a/modules/fieldlinessequence/CMakeLists.txt b/modules/fieldlinessequence/CMakeLists.txt index 5721c44e97..6bbda6d7f1 100644 --- a/modules/fieldlinessequence/CMakeLists.txt +++ b/modules/fieldlinessequence/CMakeLists.txt @@ -26,6 +26,7 @@ include(${PROJECT_SOURCE_DIR}/support/cmake/module_definition.cmake) set(HEADER_FILES rendering/renderablefieldlinessequence.h + tasks/kameleonvolumetofieldlinestask.h util/fieldlinesstate.h util/commons.h util/kameleonfieldlinehelper.h @@ -34,6 +35,7 @@ source_group("Header Files" FILES ${HEADER_FILES}) set(SOURCE_FILES rendering/renderablefieldlinessequence.cpp + tasks/kameleonvolumetofieldlinestask.cpp util/fieldlinesstate.cpp util/commons.cpp util/kameleonfieldlinehelper.cpp diff --git a/modules/fieldlinessequence/fieldlinessequencemodule.cpp b/modules/fieldlinessequence/fieldlinessequencemodule.cpp index 13de0c2083..6aef48fc51 100644 --- a/modules/fieldlinessequence/fieldlinessequencemodule.cpp +++ b/modules/fieldlinessequence/fieldlinessequencemodule.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -58,17 +59,25 @@ FieldlinesSequenceModule::FieldlinesSequenceModule() : OpenSpaceModule(Name) { } void FieldlinesSequenceModule::internalInitialize(const ghoul::Dictionary&) { - ghoul::TemplateFactory* factory = + ghoul::TemplateFactory* fRenderable = FactoryManager::ref().factory(); - ghoul_assert(factory, "No renderable factory existed"); + ghoul_assert(fRenderable, "No renderable factory existed"); + fRenderable->registerClass( + "RenderableFieldlinesSequence" + ); - factory->registerClass("RenderableFieldlinesSequence"); + ghoul::TemplateFactory* fTask = FactoryManager::ref().factory(); + ghoul_assert(fTask, "No task factory existed"); + fTask->registerClass( + "KameleonVolumeToFieldlinesTask" + ); } std::vector FieldlinesSequenceModule::documentations() const { return { - RenderableFieldlinesSequence::Documentation() + RenderableFieldlinesSequence::Documentation(), + KameleonVolumeToFieldlinesTask::Documentation() }; } diff --git a/modules/fieldlinessequence/include.cmake b/modules/fieldlinessequence/include.cmake index f6e8111e4e..e6b3e90224 100644 --- a/modules/fieldlinessequence/include.cmake +++ b/modules/fieldlinessequence/include.cmake @@ -1,3 +1,4 @@ +set(DEFAULT_MODULE ON) set (OPENSPACE_DEPENDENCIES kameleon ) diff --git a/modules/fieldlinessequence/rendering/renderablefieldlinessequence.cpp b/modules/fieldlinessequence/rendering/renderablefieldlinessequence.cpp index cdd30c069f..1b482e3a36 100644 --- a/modules/fieldlinessequence/rendering/renderablefieldlinessequence.cpp +++ b/modules/fieldlinessequence/rendering/renderablefieldlinessequence.cpp @@ -26,160 +26,188 @@ #include #include +#include #include #include -#include -#include #include -#include #include #include #include -#include -#include #include #include #include -#include -#include -#include -#include +#include #include namespace { constexpr std::string_view _loggerCat = "RenderableFieldlinesSequence"; + double extractTriggerTimeFromFilename(const std::filesystem::path& filePath) { + // number of characters in filename (excluding '.osfls') + std::string fileName = filePath.stem().string(); // excludes extention + + // Ensure the separators are correct + fileName.replace(4, 1, "-"); + fileName.replace(7, 1, "-"); + fileName.replace(10, 1, "T"); + fileName.replace(13, 1, ":"); + fileName.replace(16, 1, ":"); + fileName.replace(19, 1, "."); + return openspace::Time::convertTime(fileName); + } + constexpr openspace::properties::Property::PropertyInfo ColorMethodInfo = { "ColorMethod", "Color Method", "Color lines uniformly or using color tables based on extra quantities like, for " "examples, temperature or particle density.", - openspace::properties::Property::Visibility::AdvancedUser + openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo ColorQuantityInfo = { "ColorQuantity", "Quantity to Color By", "Quantity used to color lines if the 'By Quantity' color method is selected.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo ColorMinMaxInfo = { "ColorQuantityMinMax", "ColorTable Min Value", "Value to map to the lowest and highest end of the color table.", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo ColorTablePathInfo = { "ColorTablePath", "Path to Color Table", "Color Table/Transfer Function to use for 'By Quantity' coloring.", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo ColorUniformInfo = { "Color", "Uniform Line Color", "The uniform color of lines shown when 'Color Method' is set to 'Uniform'.", openspace::properties::Property::Visibility::NoviceUser }; + constexpr openspace::properties::Property::PropertyInfo ColorUseABlendingInfo = { "ABlendingEnabled", "Additive Blending", "Activate/deactivate additive blending.", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo DomainEnabledInfo = { "DomainEnabled", "Domain Limits", "Enable/Disable domain limits.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo DomainXInfo = { "LimitsX", "X-limits", "Valid range along the X-axis. [Min, Max].", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo DomainYInfo = { "LimitsY", "Y-limits", "Valid range along the Y-axis. [Min, Max].", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo DomainZInfo = { "LimitsZ", "Z-limits", "Valid range along the Z-axis. [Min, Max].", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo DomainRInfo = { "LimitsR", "Radial limits", "Valid radial range. [Min, Max].", openspace::properties::Property::Visibility::AdvancedUser }; + + constexpr openspace::properties::Property::PropertyInfo FlowEnabledInfo = { + "FlowEnabled", + "Flow Enabled", + "Toggles the rendering of moving particles along the lines. Can, for example, " + "illustrate magnetic flow.", + openspace::properties::Property::Visibility::NoviceUser + }; + constexpr openspace::properties::Property::PropertyInfo FlowColorInfo = { "FlowColor", "Flow Color", "Color of particles flow direction indication.", openspace::properties::Property::Visibility::NoviceUser }; - constexpr openspace::properties::Property::PropertyInfo FlowEnabledInfo = { - "FlowEnabled", - "Flow Direction", - "Toggles the rendering of moving particles along the lines. Can, for example, " - "illustrate magnetic flow.", - openspace::properties::Property::Visibility::NoviceUser - }; + constexpr openspace::properties::Property::PropertyInfo FlowReversedInfo = { "Reversed", "Reversed Flow", "Toggle to make the flow move in the opposite direction.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo FlowParticleSizeInfo = { "ParticleSize", "Particle Size", "Size of the particles.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo FlowParticleSpacingInfo = { "ParticleSpacing", "Particle Spacing", "Spacing inbetween particles.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo FlowSpeedInfo = { "Speed", "Speed", "Speed of the flow.", openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo MaskingEnabledInfo = { "MaskingEnabled", - "Masking", + "Masking Enabled", "Enable/disable masking. Use masking to show lines where a given quantity is " "within a given range, for example, if you only want to see where the " "temperature is between 10 and 20 degrees. Also used for masking out line " "topologies like solar wind & closed lines.", - openspace::properties::Property::Visibility::AdvancedUser + openspace::properties::Property::Visibility::User }; + constexpr openspace::properties::Property::PropertyInfo MaskingMinMaxInfo = { "MaskingMinLimit", "Lower Limit", "Lower and upper limit of the valid masking range.", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo MaskingQuantityInfo = { "MaskingQuantity", "Quantity used for Masking", "Quantity used for masking.", openspace::properties::Property::Visibility::AdvancedUser }; + constexpr openspace::properties::Property::PropertyInfo LineWidthInfo = { "LineWidth", "Line Width", "This value specifies the line width of the fieldlines.", openspace::properties::Property::Visibility::NoviceUser }; + constexpr openspace::properties::Property::PropertyInfo TimeJumpButtonInfo = { "TimeJumpToStart", "Jump to Start Of Sequence", @@ -187,38 +215,26 @@ namespace { openspace::properties::Property::Visibility::NoviceUser }; + constexpr openspace::properties::Property::PropertyInfo SaveDownloadsOnShutdown = { + "SaveDownloadsOnShutdown", + "Save Downloads On Shutdown", + "This is an option for if dynamically downloaded should be saved between runs " + "or not.", + openspace::properties::Property::Visibility::User + }; + struct [[codegen::Dictionary(RenderableFieldlinesSequence)]] Parameters { - enum class SourceFileType { - Cdf, - Json, - Osfls + enum class [[codegen::map(openspace::RenderableFieldlinesSequence::ColorMethod)]] + ColorMethod { + Uniform, + ByQuantity [[codegen::key("By Quantity")]] }; - // Input file type. Should be cdf, json or osfls - SourceFileType inputFileType; - // Path to folder containing the input files - std::filesystem::path sourceFolder [[codegen::directory()]]; + // [[codegen::verbatim(ColorMethodInfo.description)]] + std::optional colorMethod; - // Path to a .txt file containing seed points. Mandatory if CDF as input. - // Files need time stamp in file name like so: yyyymmdd_hhmmss.txt - std::optional seedPointDirectory [[codegen::directory()]]; - - // Currently supports: batsrus, enlil & pfss - std::optional simulationModel; - - // Extra variables such as rho, p or t - std::optional> extraVariables; - - // Which variable in CDF file to trace. b is default for fieldline - std::optional tracingVariable; - - // Convert the models distance unit, ex. AU for Enlil, to meters. - // Can be used during runtime to scale domain limits. - // 1.f is default, assuming meters as input. - std::optional scaleToMeters; - - // Set to true if you are streaming data during runtime - std::optional loadAtRuntime; + // [[codegen::verbatim(ColorQuantityInfo.description)]] + std::optional colorQuantity; // [[codegen::verbatim(ColorUniformInfo.description)]] std::optional color [[codegen::color()]]; @@ -227,18 +243,14 @@ namespace { // used for colorizing the fieldlines according to different parameters std::optional> colorTablePaths; - // [[codegen::verbatim(ColorMethodInfo.description)]] - std::optional colorMethod; - - // [[codegen::verbatim(ColorQuantityInfo.description)]] - std::optional colorQuantity; - - // List of ranges for which their corresponding parameters values will be - // colorized by. Should be entered as {min value, max value} per range + // Ranges for which their corresponding parameters values will be + // colorized by. Should be entered as min value, max value std::optional> colorTableRanges; - // Enables flow, showing the direction, but not accurate speed, that particles - // would be traveling + // Specifies the total data range to where color map will be applied + std::optional colorMinMaxRange; + + // [[codegen::verbatim(FlowEnabledInfo.description)]] std::optional flowEnabled; // [[codegen::verbatim(FlowColorInfo.description)]] @@ -262,36 +274,108 @@ namespace { // [[codegen::verbatim(MaskingQuantityInfo.description)]] std::optional maskingQuantity; - // List of ranges for which their corresponding parameters values will be - // masked by. Should be entered as {min value, max value} per range + // Ranges for which their corresponding quantity parameter value will be + // masked by. Should be entered as a min value, max value pair. std::optional> maskingRanges; + // Ranges for which their corresponding parameters values will be + // masked by. Should be entered as a min value, max value pair. + std::optional maskingMinMaxRange; + // [[codegen::verbatim(DomainEnabledInfo.description)]] std::optional domainEnabled; - // Value should be path to folder where states are saved. Specifying this - // makes it use file type converter - // (JSON/CDF input => osfls output & oslfs input => JSON output) - std::optional outputFolder; - // [[codegen::verbatim(LineWidthInfo.description)]] std::optional lineWidth; + // [[codegen::verbatim(ColorUseABlendingInfo.description)]] + std::optional alphaBlendingEnabled; + + // Set if first/last file should render forever. + bool showAtAllTimes; + // If data sets parameter start_time differ from start of run, // elapsed_time_in_seconds might be in relation to start of run. // ManuelTimeOffset will be added to trigger time. - std::optional manualTimeOffset; + std::optional manualTimeOffset; + + enum class [[codegen::map(openspace::fls::Model)]] Model { + Batsrus, + Enlil, + Pfss + }; + + // If the simulation model is not specified, it means that the scaleFactor + // (scaleToMeters) will be 1.0 assuming meter as input. + std::optional simulationModel; + + // Convert the models distance unit, ex. AU to meters for Enlil. + // 1.0 is default, assuming meters as input. + // Does not need to be specified if simulationModel is specified. + // When using a different model, use this value to scale your vertex positions to + // meters. + std::optional scaleToMeters; + + enum class [[codegen::map(openspace::RenderableFieldlinesSequence::LoadingType)]] + LoadingType { + StaticLoading, + DynamicDownloading + }; + + // Choose type of loading: + // `StaticLoading`: Download and load files on startup. + // `DynamicDownloading`: Download and load files during run time. + std::optional loadingType; + + // A data ID that corresponds to what dataset to use if using dynamic data + // downloading. + std::optional dataID; + + // A maximum number to limit the number of files being downloaded simultaneously. + std::optional numberOfFilesToQueue; + + // A URL to a JSON-formatted page with metadata for the `DataURL`. + // Required if using dynamic downloading. + std::optional infoURL; + + // A URL to a JSON-formatted page with a list of each available data file. + // Required if using dynamic downloading. + std::optional dataURL; + + enum class + [[codegen::map(openspace::RenderableFieldlinesSequence::SourceFileType)]] + SourceFileType { + Cdf, + Json, + Osfls + }; + + // Specify the file format of the data used. + SourceFileType inputFileType; + + // Path to folder containing the input files. + std::optional sourceFolder [[codegen::directory()]]; + + // Path to a directory including .txt files that contain seed points. The files + // need a file name with a timestamp in the format: yyyymmdd_hhmmss.txt. + // Required if CDF is used as input. + std::optional seedPointDirectory [[codegen::directory()]]; + + // Extra variables that can be used to color the field lines. + std::optional> extraVariables; + + // Which variable in CDF file to trace. + std::optional tracingVariable; + + // Decides whether or not to cache the downloaded data between runs. By default, + // caching is disabled and all downloaded content will be deleted when OpenSpace + // is shut down. Set to true to save all the downloaded files. + std::optional cacheData; }; #include "renderablefieldlinessequence_codegen.cpp" } // namespace namespace openspace { -fls::Model stringToModel(std::string str); -std::unordered_map> - extractSeedPointsFromFiles(std::filesystem::path); -std::vector - extractMagnitudeVarsFromStrings(std::vector extrVars); - documentation::Documentation RenderableFieldlinesSequence::Documentation() { return codegen::doc("fieldlinessequence_renderablefieldlinessequence"); } @@ -302,9 +386,9 @@ RenderableFieldlinesSequence::RenderableFieldlinesSequence( , _colorGroup({ "Color" }) , _colorMethod(ColorMethodInfo) , _colorQuantity(ColorQuantityInfo) - , _colorQuantityMinMax( + , _selectedColorRange( ColorMinMaxInfo, - glm::vec2(-0.f, 100.f), + glm::vec2(0.f, 100.f), glm::vec2(-5000.f), glm::vec2(5000.f) ) @@ -316,27 +400,27 @@ RenderableFieldlinesSequence::RenderableFieldlinesSequence( glm::vec4(1.f) ) , _colorABlendEnabled(ColorUseABlendingInfo, true) - , _domainEnabled(DomainEnabledInfo, true) + , _domainEnabled(DomainEnabledInfo, false) , _domainGroup({ "Domain" }) , _domainX(DomainXInfo) , _domainY(DomainYInfo) , _domainZ(DomainZInfo) , _domainR(DomainRInfo) + , _flowEnabled(FlowEnabledInfo, false) + , _flowGroup({ "Flow" }) , _flowColor( FlowColorInfo, - glm::vec4(0.96f, 0.88f, 0.8f, 0.5f), + glm::vec4(0.96f, 0.88f, 0.8f, 1.f), glm::vec4(0.f), glm::vec4(1.f) ) - , _flowEnabled(FlowEnabledInfo, false) - , _flowGroup({ "Flow" }) , _flowParticleSize(FlowParticleSizeInfo, 5, 0, 500) , _flowParticleSpacing(FlowParticleSpacingInfo, 60, 0, 500) , _flowReversed(FlowReversedInfo, false) , _flowSpeed(FlowSpeedInfo, 20, 0, 1000) , _maskingEnabled(MaskingEnabledInfo, false) , _maskingGroup({ "Masking" }) - , _maskingMinMax( + , _selectedMaskingRange( MaskingMinMaxInfo, glm::vec2(0.f, 100.f), glm::vec2(-5000.f), @@ -344,77 +428,97 @@ RenderableFieldlinesSequence::RenderableFieldlinesSequence( ) , _maskingQuantity(MaskingQuantityInfo) , _lineWidth(LineWidthInfo, 1.f, 1.f, 20.f) - , _jumpToStartBtn(TimeJumpButtonInfo) + , _jumpToStart(TimeJumpButtonInfo) + , _saveDownloadsOnShutdown(SaveDownloadsOnShutdown, false) { const Parameters p = codegen::bake(dictionary); addProperty(Fadeable::_opacity); - // Extracts the general information (from the asset file) that - // is mandatory for the class to function; - std::string fileTypeString; - switch (p.inputFileType) { - case Parameters::SourceFileType::Cdf: - _inputFileType = SourceFileType::Cdf; - fileTypeString = "cdf"; - if (p.tracingVariable.has_value()) { - _tracingVariable = *p.tracingVariable; + _inputFileType = codegen::map(p.inputFileType); + if (p.loadingType.has_value()) { + _loadingType = codegen::map(*p.loadingType); + } + else { + _loadingType = LoadingType::StaticLoading; + } + + if (_loadingType == LoadingType::DynamicDownloading && + _inputFileType == SourceFileType::Cdf) + { + throw ghoul::RuntimeError( + "Dynamic loading (or downloading) is only supported for .osfls and .json " + "files" + ); + } + if (_loadingType == LoadingType::StaticLoading && !p.sourceFolder.has_value()) { + throw ghoul::RuntimeError( + "Either dynamic downloading parameters or a sync folder must be specified" + ); + } + + if (p.simulationModel.has_value()) { + _model = codegen::map(*p.simulationModel); + } + else { + _model = fls::Model::Invalid; + } + + setModelDependentConstants(); + + // Setting the scaling factor after model to support the case with unknown models + // (model = invalid), but scaling factor being specified. + _scalingFactor = p.scaleToMeters.value_or(_scalingFactor); + + if (_loadingType == LoadingType::DynamicDownloading) { + if (!p.dataID.has_value()) { + throw ghoul::RuntimeError( + "If running with dynamic downloading, DataID needs to be specified" + ); + } + _dataID = *p.dataID; + + _nFilesToQueue = static_cast( + p.numberOfFilesToQueue.value_or(_nFilesToQueue) + ); + + if (!p.infoURL.has_value()) { + throw ghoul::RuntimeError("InfoURL has to be provided"); + } + _infoURL = *p.infoURL; + + if (!p.dataURL.has_value()) { + throw ghoul::RuntimeError("DataURL has to be provided"); + } + _dataURL = *p.dataURL; + } + else { + ghoul_assert( + p.sourceFolder.has_value(), + "sourceFolder not specified though it should not be able to get here" + ); + std::filesystem::path path = p.sourceFolder.value(); + namespace fs = std::filesystem; + for (const fs::directory_entry& e : fs::directory_iterator(path)) { + if (!e.is_regular_file()) { + continue; } - else { - _tracingVariable = "b"; // Magnetic field variable as default - LWARNING(std::format( - "No tracing variable, using default '{}'", _tracingVariable + File file = { + .status = File::FileStatus::Downloaded, + .path = e.path(), + .timestamp = -1.0 + }; + _files.push_back(std::move(file)); + if (_files[0].path.empty()) { + throw ghoul::RuntimeError(std::format( + "Error finding file '{}' in folder '{}'", + e.path().filename(), path )); } - break; - case Parameters::SourceFileType::Json: - _inputFileType = SourceFileType::Json; - fileTypeString = "json"; - break; - case Parameters::SourceFileType::Osfls: - _inputFileType = SourceFileType::Osfls; - fileTypeString = "osfls"; - break; - } - - // Ensure that the source folder exists and then extract - // the files with the same extension as fileTypeString - std::filesystem::path sourcePath = p.sourceFolder; - if (!std::filesystem::is_directory(sourcePath)) { - LERROR(std::format( - "FieldlinesSequence '{}' is not a valid directory", sourcePath - )); - } - - // Extract all file paths from the provided folder - namespace fs = std::filesystem; - for (const fs::directory_entry& e : fs::directory_iterator(sourcePath)) { - if (e.is_regular_file()) { - std::string eStr = e.path().string(); - _sourceFiles.push_back(eStr); } + _maxLoadedFiles = _files.size(); } - std::sort(_sourceFiles.begin(), _sourceFiles.end()); - // Remove all files that don't have fileTypeString as file extension - _sourceFiles.erase( - std::remove_if( - _sourceFiles.begin(), - _sourceFiles.end(), - [&fileTypeString](const std::string& str) { - const size_t extLength = fileTypeString.length(); - std::string sub = str.substr(str.length() - extLength, extLength); - sub = ghoul::toLowerCase(sub); - return sub != fileTypeString; - } - ), - _sourceFiles.end() - ); - - // Ensure that there are available and valid source files left - if (_sourceFiles.empty()) { - LERROR(std::format("'{}' contains no {} files", sourcePath, fileTypeString)); - } _extraVars = p.extraVariables.value_or(_extraVars); _flowEnabled = p.flowEnabled.value_or(_flowEnabled); _flowColor = p.flowColor.value_or(_flowColor); @@ -422,129 +526,221 @@ RenderableFieldlinesSequence::RenderableFieldlinesSequence( _flowParticleSize = p.particleSize.value_or(_flowParticleSize); _flowParticleSpacing = p.particleSpacing.value_or(_flowParticleSpacing); _flowSpeed = p.flowSpeed.value_or(_flowSpeed); - _lineWidth = p.lineWidth.value_or(_lineWidth); - _manualTimeOffset = p.manualTimeOffset.value_or(_manualTimeOffset); - _modelStr = p.simulationModel.value_or(_modelStr); - _seedPointDirectory = p.seedPointDirectory.value_or(_seedPointDirectory); _maskingEnabled = p.maskingEnabled.value_or(_maskingEnabled); _maskingQuantityTemp = p.maskingQuantity.value_or(_maskingQuantityTemp); + _domainEnabled = p.domainEnabled.value_or(_domainEnabled); + _lineWidth = p.lineWidth.value_or(_lineWidth); + _colorABlendEnabled = p.alphaBlendingEnabled.value_or(_colorABlendEnabled); + _renderForever = p.showAtAllTimes; + _manualTimeOffset = p.manualTimeOffset.value_or(_manualTimeOffset); + _saveDownloadsOnShutdown = p.cacheData.value_or(_saveDownloadsOnShutdown); + + if (_loadingType == LoadingType::StaticLoading){ + staticallyLoadFiles(p.seedPointDirectory, p.tracingVariable); + computeSequenceEndTime(); + } + // Color group + _colorTablePath = FieldlinesSequenceModule::DefaultTransferFunctionFile.string(); if (p.colorTablePaths.has_value()) { - _colorTablePaths = *p.colorTablePaths; + for (const std::filesystem::path& path : *p.colorTablePaths) { + if (!std::filesystem::exists(path)) { + throw ghoul::RuntimeError(std::format( + "Color table path '{}' is not a valid file", path + )); + } + _colorTablePaths.emplace_back(path); + } } - else { - // Set a default color table, just in case the (optional) user defined paths are - // corrupt or not provided - _colorTablePaths.push_back(FieldlinesSequenceModule::DefaultTransferFunctionFile); + if (!p.colorTablePaths.has_value() || _colorTablePaths.empty()) { + _colorTablePaths.emplace_back( + FieldlinesSequenceModule::DefaultTransferFunctionFile + ); } - _colorUniform = p.color.value_or(_colorUniform); - _colorMethod.addOption(static_cast(ColorMethod::Uniform), "Uniform"); _colorMethod.addOption(static_cast(ColorMethod::ByQuantity), "By Quantity"); if (p.colorMethod.has_value()) { - if (p.colorMethod.value() == "Uniform") { - _colorMethod = static_cast(ColorMethod::Uniform); - } - else { - _colorMethod = static_cast(ColorMethod::ByQuantity); - } + _colorMethod = static_cast( + codegen::map( + *p.colorMethod + ) + ); } else { _colorMethod = static_cast(ColorMethod::Uniform); } - - if (p.colorQuantity.has_value()) { - _colorMethod = static_cast(ColorMethod::ByQuantity); - _colorQuantityTemp = *p.colorQuantity; - } + _colorQuantityTemp = p.colorQuantity.value_or(_colorQuantityTemp); if (p.colorTableRanges.has_value()) { _colorTableRanges = *p.colorTableRanges; } else { _colorTableRanges.push_back(glm::vec2(0.f, 1.f)); + _selectedColorRange = glm::vec2(0.f, 1.f); } - _loadingStatesDynamically = p.loadAtRuntime.value_or(_loadingStatesDynamically); - if (_loadingStatesDynamically && _inputFileType != SourceFileType::Osfls) { - LWARNING("Load at run time is only supported for osfls file type"); - _loadingStatesDynamically = false; + if (p.colorMinMaxRange.has_value()) { + _selectedColorRange.setMinValue(glm::vec2(p.colorMinMaxRange->x)); + _selectedColorRange.setMaxValue(glm::vec2(p.colorMinMaxRange->y)); } - if (p.maskingRanges.has_value()) { _maskingRanges = *p.maskingRanges; } else { - _maskingRanges.push_back(glm::vec2(-100000.f, 100000.f)); // some default values + _maskingRanges.push_back(glm::vec2(0.f, 1.f)); + _selectedMaskingRange = glm::vec2(0.f, 1.f); } - _domainEnabled = p.domainEnabled.value_or(_domainEnabled); - - _outputFolderPath = p.outputFolder.value_or(_outputFolderPath); - if (!_outputFolderPath.empty() && !std::filesystem::is_directory(_outputFolderPath)) { - _outputFolderPath.clear(); - LERROR(std::format( - "The specified output path '{}' does not exist", _outputFolderPath - )); + if (p.maskingMinMaxRange.has_value()) { + _selectedMaskingRange.setMinValue(glm::vec2(p.maskingMinMaxRange->x)); + _selectedMaskingRange.setMaxValue(glm::vec2(p.maskingMinMaxRange->y)); } - _scalingFactor = p.scaleToMeters.value_or(_scalingFactor); + _colorQuantity.onChange([this]() { + if (_colorTablePaths.empty()) { + return; + } + _shouldUpdateColorBuffer = true; + // Note that we do not need to set _selectedColorRange in the constructor, due to + // this onChange being declared before firstupdate() function that sets + // _colorQuantity. + if (_colorTableRanges.size() > _colorQuantity) { + _selectedColorRange = _colorTableRanges[_colorQuantity]; + } + // If fewer data ranges are given than there are parameters in the data, use the + // first range. + // @TODO (2025-06-10, elon) We should use a better structure for providing the + // data, since this creates discrepancy for which range belongs to which parameter + else { + _selectedColorRange = _colorTableRanges[0]; + } + + if (_colorTablePaths.size() > _colorQuantity) { + _colorTablePath = _colorTablePaths[_colorQuantity].string(); + } + else { + _colorTablePath = _colorTablePaths[0].string(); + } + }); + + // This is to save the changes done in the gui for when you switch between options + _selectedColorRange.onChange([this]() { + if (_colorTableRanges.size() > _colorQuantity) { + _colorTableRanges[_colorQuantity] = _selectedColorRange; + } + }); + + _colorTablePath.onChange([this]() { + if (std::filesystem::exists(_colorTablePath.value())) { + _transferFunction = + std::make_unique(_colorTablePath.value()); + } + else { + LERROR(std::format( + "Invalid path '{}' to transfer function. Please enter new path", + _colorTablePath.value() + )); + } + }); + + _maskingQuantity.onChange([this]() { + _shouldUpdateMaskingBuffer = true; + if (_maskingRanges.size() > _maskingQuantity) { + _selectedMaskingRange = _maskingRanges[_maskingQuantity]; + } + else if (!_maskingRanges.empty()) { + _selectedMaskingRange = _maskingRanges[0]; + } + else { + LERROR("Cannot set selected masking range"); + } + }); + + _selectedMaskingRange.onChange([this]() { + if (_maskingRanges.size() > _maskingQuantity) { + _maskingRanges[_maskingQuantity] = _selectedMaskingRange; + } + }); + + _jumpToStart.onChange([this]() { + if (_atLeastOneFileLoaded) { + global::timeManager->setTimeNextFrame(Time(_files[0].timestamp)); + } + }); + setupProperties(); +} + +void RenderableFieldlinesSequence::staticallyLoadFiles( + const std::optional& seedPointDirectory, + const std::optional& tracingVariable) +{ + bool loadSuccess = false; + switch (_inputFileType) { + case SourceFileType::Cdf: + _seedPointDirectory = seedPointDirectory.value_or(_seedPointDirectory); + _tracingVariable = tracingVariable.value_or(_tracingVariable); + for (File& file : _files) { + if (!tracingVariable.has_value()) { + throw ghoul::RuntimeError("No tracing variable specified"); + } + std::vector extraMagVars = + fls::extractMagnitudeVarsFromStrings(_extraVars); + std::unordered_map> seedsPerFiles = + fls::extractSeedPointsFromFiles(_seedPointDirectory); + if (seedsPerFiles.empty()) { + throw ghoul::RuntimeError("No seed files found"); + } + loadSuccess = fls::convertCdfToFieldlinesState( + file.state, + file.path.string(), + seedsPerFiles, + _manualTimeOffset, + _tracingVariable, + _extraVars, + extraMagVars + ); + } + break; + case SourceFileType::Json: + for (File& file : _files) { + loadSuccess = file.state.loadStateFromJson( + file.path.string(), + _model, + _scalingFactor + ); + } + break; + case SourceFileType::Osfls: + for (File& file : _files) { + loadFile(file); + } + break; + default: + throw ghoul::MissingCaseException(); + } + + _isLoadingStateFromDisk = false; + for (File& file : _files) { + if (!file.path.empty() && file.status != File::FileStatus::Loaded) { + file.status = File::FileStatus::Loaded; + file.timestamp = extractTriggerTimeFromFilename(file.path); + _atLeastOneFileLoaded = true; + } + } + std::sort(_files.begin(), _files.end()); } void RenderableFieldlinesSequence::initialize() { - _transferFunction = std::make_unique(absPath(_colorTablePaths[0])); + _isFirstLoad = true; } void RenderableFieldlinesSequence::initializeGL() { - // Setup shader program _shaderProgram = global::renderEngine->buildRenderProgram( "FieldlinesSequence", absPath("${MODULE_FIELDLINESSEQUENCE}/shaders/fieldlinessequence_vs.glsl"), absPath("${MODULE_FIELDLINESSEQUENCE}/shaders/fieldlinessequence_fs.glsl") ); - // Extract source file type specific information from dictionary - // & get states from source - switch (_inputFileType) { - case SourceFileType::Cdf: - if (!getStatesFromCdfFiles()) { - return; - } - break; - case SourceFileType::Json: - if (!loadJsonStatesIntoRAM()) { - return; - } - break; - case SourceFileType::Osfls: - if (_loadingStatesDynamically) { - if (!prepareForOsflsStreaming()) { - return; - } - } - else { - loadOsflsStatesIntoRAM(); - } - break; - default: - return; - } - - // No need to store source paths in memory if they are already in RAM - if (!_loadingStatesDynamically) { - _sourceFiles.clear(); - } - - // At this point there should be at least one state loaded into memory - if (_states.empty()) { - LERROR("Wasn't able to extract any valid states from provided source files"); - return; - } - - computeSequenceEndTime(); - setModelDependentConstants(); - setupProperties(); - glGenVertexArrays(1, &_vertexArrayObject); glGenBuffers(1, &_vertexPositionBuffer); glGenBuffers(1, &_vertexColorBuffer); @@ -554,188 +750,50 @@ void RenderableFieldlinesSequence::initializeGL() { setRenderBin(Renderable::RenderBin::Overlay); } -// Returns fls::Model::Invalid if it fails to extract mandatory information -fls::Model stringToModel(std::string str) { - str = ghoul::toLowerCase(str); - return fls::stringToModel(str); -} - -bool RenderableFieldlinesSequence::loadJsonStatesIntoRAM() { - fls::Model model = stringToModel(_modelStr); - for (const std::string& filePath : _sourceFiles) { - FieldlinesState newState; - const bool loadedSuccessfully = newState.loadStateFromJson( - filePath, - model, - _scalingFactor - ); - if (loadedSuccessfully) { - addStateToSequence(newState); - if (!_outputFolderPath.empty()) { - newState.saveStateToOsfls(_outputFolderPath); - } - } - } - return true; -} - -bool RenderableFieldlinesSequence::prepareForOsflsStreaming() { - extractTriggerTimesFromFileNames(); - FieldlinesState newState; - if (!newState.loadStateFromOsfls(_sourceFiles[0])) { - LERROR("The provided .osfls files seem to be corrupt"); - return false; - } - _states.push_back(newState); - _nStates = _startTimes.size(); - if (_nStates == 1) { - // loading dynamicaly is not nessesary if only having one set in the sequence - _loadingStatesDynamically = false; - } - _activeStateIndex = 0; - return true; -} - -void RenderableFieldlinesSequence::loadOsflsStatesIntoRAM() { - for (const std::string& filePath : _sourceFiles) { - FieldlinesState newState; - if (newState.loadStateFromOsfls(filePath)) { - addStateToSequence(newState); - if (!_outputFolderPath.empty()) { - newState.saveStateToJson( - _outputFolderPath + std::filesystem::path(filePath).stem().string() - ); - } - } - else { - LWARNING(std::format("Failed to load state from '{}'", filePath)); - } - } -} - void RenderableFieldlinesSequence::setupProperties() { - bool hasExtras = (_states[0].nExtraQuantities() > 0); - - // Add non-grouped properties (enablers and buttons) addProperty(_colorABlendEnabled); - addProperty(_domainEnabled); - addProperty(_flowEnabled); - if (hasExtras) { - addProperty(_maskingEnabled); - } addProperty(_lineWidth); - addProperty(_jumpToStartBtn); + addProperty(_jumpToStart); // Add Property Groups addPropertySubOwner(_colorGroup); addPropertySubOwner(_domainGroup); addPropertySubOwner(_flowGroup); - if (hasExtras) { - addPropertySubOwner(_maskingGroup); - } + addPropertySubOwner(_maskingGroup); - // Add Properties to the groups _colorUniform.setViewOption(properties::Property::ViewOptions::Color); _colorGroup.addProperty(_colorUniform); + _colorGroup.addProperty(_colorMethod); + _colorGroup.addProperty(_colorQuantity); + _selectedColorRange.setViewOption(properties::Property::ViewOptions::MinMaxRange); + _colorGroup.addProperty(_selectedColorRange); + _colorGroup.addProperty(_colorTablePath); + + _domainGroup.addProperty(_domainEnabled); _domainGroup.addProperty(_domainX); _domainGroup.addProperty(_domainY); _domainGroup.addProperty(_domainZ); _domainGroup.addProperty(_domainR); + + _flowGroup.addProperty(_flowEnabled); _flowGroup.addProperty(_flowReversed); _flowColor.setViewOption(properties::Property::ViewOptions::Color); _flowGroup.addProperty(_flowColor); _flowGroup.addProperty(_flowParticleSize); _flowGroup.addProperty(_flowParticleSpacing); _flowGroup.addProperty(_flowSpeed); - if (hasExtras) { - _colorGroup.addProperty(_colorMethod); - _colorGroup.addProperty(_colorQuantity); - _colorQuantityMinMax.setViewOption( - properties::Property::ViewOptions::MinMaxRange - ); - _colorGroup.addProperty(_colorQuantityMinMax); - _colorGroup.addProperty(_colorTablePath); - _maskingGroup.addProperty(_maskingQuantity); - _maskingMinMax.setViewOption(properties::Property::ViewOptions::MinMaxRange); - _maskingGroup.addProperty(_maskingMinMax); - // Add option for each extra quantity. Assumes there are just as many names to - // extra quantities as there are extra quantities. Also assume that all states in - // the given sequence have the same extra quantities - const size_t nExtraQuantities = _states[0].nExtraQuantities(); - const std::vector& extraNamesVec = _states[0].extraQuantityNames(); - for (int i = 0; i < static_cast(nExtraQuantities); i++) { - _colorQuantity.addOption(i, extraNamesVec[i]); - _maskingQuantity.addOption(i, extraNamesVec[i]); - } - // Each quantity should have its own color table and color table range - // no more, no less - _colorTablePaths.resize(nExtraQuantities, _colorTablePaths.back()); - _colorTablePath = _colorTablePaths[0].string(); - _colorTableRanges.resize(nExtraQuantities, _colorTableRanges.back()); - _maskingRanges.resize(nExtraQuantities, _maskingRanges.back()); - } + _maskingGroup.addProperty(_maskingEnabled); + _maskingGroup.addProperty(_maskingQuantity); + _selectedMaskingRange.setViewOption(properties::Property::ViewOptions::MinMaxRange); + _maskingGroup.addProperty(_selectedMaskingRange); - definePropertyCallbackFunctions(); - - if (hasExtras) { - // Set defaults - _colorQuantity = _colorQuantityTemp; - _colorQuantityMinMax = _colorTableRanges[_colorQuantity]; - - _maskingQuantity = _maskingQuantityTemp; - _maskingMinMax = _maskingRanges[_colorQuantity]; - } -} - -void RenderableFieldlinesSequence::definePropertyCallbackFunctions() { - // Add Property Callback Functions - bool hasExtras = (_states[0].nExtraQuantities() > 0); - if (hasExtras) { - _colorQuantity.onChange([this]() { - _shouldUpdateColorBuffer = true; - _colorQuantityMinMax = _colorTableRanges[_colorQuantity]; - _colorTablePath = _colorTablePaths[_colorQuantity].string(); - }); - - _colorTablePath.onChange([this]() { - _transferFunction->setPath(_colorTablePath.value()); - }); - - _colorQuantityMinMax.onChange([this]() { - _colorTableRanges[_colorQuantity] = _colorQuantityMinMax; - }); - - _maskingQuantity.onChange([this]() { - _shouldUpdateMaskingBuffer = true; - _maskingMinMax = _maskingRanges[_maskingQuantity]; - }); - - _maskingMinMax.onChange([this]() { - _maskingRanges[_maskingQuantity] = _maskingMinMax; - }); - } - - _jumpToStartBtn.onChange([this]() { - global::timeManager->setTimeNextFrame(Time(_startTimes[0])); - }); -} - -// Calculate expected end time. -void RenderableFieldlinesSequence::computeSequenceEndTime() { - if (_nStates > 1) { - const double lastTriggerTime = _startTimes[_nStates - 1]; - const double sequenceDuration = lastTriggerTime - _startTimes[0]; - const double averageStateDuration = sequenceDuration / - (static_cast(_nStates) - 1.0); - _sequenceEndTime = lastTriggerTime + averageStateDuration; - } + addProperty(_saveDownloadsOnShutdown); } void RenderableFieldlinesSequence::setModelDependentConstants() { - const fls::Model simulationModel = _states[0].model(); float limit = 100.f; // Just used as a default value. - switch (simulationModel) { + switch (_model) { case fls::Model::Batsrus: _scalingFactor = fls::ReToMeter; limit = 300.f; // Should include a long magnetotail @@ -772,165 +830,6 @@ void RenderableFieldlinesSequence::setModelDependentConstants() { _domainR = glm::vec2(0.f, limit * 1.5f); } -// Extract J2000 time from file names -// Requires files to be named as such: 'YYYY-MM-DDTHH-MM-SS-XXX.osfls' -void RenderableFieldlinesSequence::extractTriggerTimesFromFileNames() { - // number of characters in filename (excluding '.osfls') - constexpr int FilenameSize = 23; - // size(".osfls") - constexpr int ExtSize = 6; - - for (const std::string& filePath : _sourceFiles) { - const size_t strLength = filePath.size(); - // Extract the filename from the path (without extension) - std::string timeString = filePath.substr( - strLength - FilenameSize - ExtSize, - FilenameSize - 1 - ); - // Ensure the separators are correct - timeString.replace(4, 1, "-"); - timeString.replace(7, 1, "-"); - timeString.replace(13, 1, ":"); - timeString.replace(16, 1, ":"); - timeString.replace(19, 1, "."); - const double triggerTime = Time::convertTime(timeString); - _startTimes.push_back(triggerTime); - } -} - -void RenderableFieldlinesSequence::addStateToSequence(FieldlinesState& state) { - _states.push_back(state); - _startTimes.push_back(state.triggerTime()); - ++_nStates; -} - -bool RenderableFieldlinesSequence::getStatesFromCdfFiles() { - std::vector extraMagVars = extractMagnitudeVarsFromStrings(_extraVars); - - std::unordered_map> seedsPerFiles = - extractSeedPointsFromFiles(_seedPointDirectory); - if (seedsPerFiles.empty()) { - LERROR("No seed files found"); - return false; - } - - for (const std::string& cdfPath : _sourceFiles) { - FieldlinesState newState; - bool isSuccessful = fls::convertCdfToFieldlinesState( - newState, - cdfPath, - seedsPerFiles, - _manualTimeOffset, - _tracingVariable, - _extraVars, - extraMagVars - ); - - if (isSuccessful) { - addStateToSequence(newState); - if (!_outputFolderPath.empty()) { - newState.saveStateToOsfls(_outputFolderPath); - } - } - } - return true; -} - -std::unordered_map> - extractSeedPointsFromFiles(std::filesystem::path filePath) -{ - std::unordered_map> outMap; - - if (!std::filesystem::is_directory(filePath)) { - LERROR(std::format( - "The specified seed point directory '{}' does not exist", filePath - )); - return outMap; - } - - namespace fs = std::filesystem; - for (const fs::directory_entry& spFile : fs::directory_iterator(filePath)) { - std::string seedFilePath = spFile.path().string(); - if (!spFile.is_regular_file() || - seedFilePath.substr(seedFilePath.find_last_of('.') + 1) != "txt") - { - continue; - } - - std::ifstream seedFile(seedFilePath); - if (!seedFile.good()) { - LERROR(std::format("Could not open seed points file '{}'", seedFilePath)); - outMap.clear(); - return {}; - } - - LDEBUG(std::format("Reading seed points from file '{}'", seedFilePath)); - std::string line; - std::vector outVec; - while (ghoul::getline(seedFile, line)) { - std::stringstream ss(line); - glm::vec3 point; - ss >> point.x; - ss >> point.y; - ss >> point.z; - outVec.push_back(std::move(point)); - } - - if (outVec.empty()) { - LERROR(std::format("Found no seed points in '{}'", seedFilePath)); - outMap.clear(); - return {}; - } - - size_t lastIndex = seedFilePath.find_last_of('.'); - std::string name = seedFilePath.substr(0, lastIndex); // remove file extention - size_t dateAndTimeSeperator = name.find_last_of('_'); - std::string time = name.substr(dateAndTimeSeperator + 1, name.length()); - std::string date = name.substr(dateAndTimeSeperator - 8, 8); // 8 for yyyymmdd - std::string dateAndTime = date + time; - - // add outVec as value and time stamp as int as key - outMap[dateAndTime] = outVec; - } - return outMap; -} - -std::vector - extractMagnitudeVarsFromStrings(std::vector extrVars) -{ - std::vector extraMagVars; - for (int i = 0; i < static_cast(extrVars.size()); i++) { - const std::string& str = extrVars[i]; - // Check if string is in the format specified for magnitude variables - if (str.substr(0, 2) == "|(" && str.substr(str.size() - 2, 2) == ")|") { - std::istringstream ss(str.substr(2, str.size() - 4)); - std::string magVar; - size_t counter = 0; - while (ghoul::getline(ss, magVar, ',')) { - magVar.erase( - std::remove_if( - magVar.begin(), - magVar.end(), - ::isspace - ), - magVar.end() - ); - extraMagVars.push_back(magVar); - counter++; - if (counter == 3) { - break; - } - } - if (counter != 3 && counter > 0) { - extraMagVars.erase(extraMagVars.end() - counter, extraMagVars.end()); - } - extrVars.erase(extrVars.begin() + i); - i--; - } - } - return extraMagVars; -} - void RenderableFieldlinesSequence::deinitializeGL() { glDeleteVertexArrays(1, &_vertexArrayObject); _vertexArrayObject = 0; @@ -949,14 +848,112 @@ void RenderableFieldlinesSequence::deinitializeGL() { _shaderProgram = nullptr; } - // Stall main thread until thread that's loading states is done - bool printedWarning = false; - while (_isLoadingStateFromDisk) { - if (!printedWarning) { - LWARNING("Trying to destroy class when an active thread is still using it"); - printedWarning = true; + _files.clear(); + + if (_loadingType == LoadingType::DynamicDownloading && _dynamicFileDownloader) { + _dynamicFileDownloader->deinitialize(_saveDownloadsOnShutdown); + } +} + +void RenderableFieldlinesSequence::computeSequenceEndTime() { + if (_files.empty()) { + _sequenceEndTime = 0.f; + } + else if (_files.size() == 1) { + _sequenceEndTime = _files[0].timestamp + 7200.f; + if (_loadingType == LoadingType::StaticLoading && !_renderForever) { + // TODO (2025-06-10, Elon) Alternativly check at construction and throw + // exeption. + LWARNING( + "Only one file in data set, but ShowAtAllTimes set to false. Using a 2h " + "window after the files time stamp to visualize the data" + ); } - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + else if (_files.size() > 1) { + const double lastTriggerTime = _files.back().timestamp; + const double sequenceDuration = lastTriggerTime - _files[0].timestamp; + const double averageCadence = sequenceDuration / (_files.size() - 1); + // A multiplier of 3 to the average cadence is added at the end as a buffer + // 3 because if you start it just before new data came in, you might just be + // outside the sequence end time otherwise + _sequenceEndTime = lastTriggerTime + 3 * averageCadence; + } +} + +void RenderableFieldlinesSequence::loadFile(File& file) { + _isLoadingStateFromDisk = true; + try { + if (_inputFileType == SourceFileType::Osfls) { + file.state = FieldlinesState::createStateFromOsfls(file.path.string()); + } + else if (_inputFileType == SourceFileType::Json) { + file.state.loadStateFromJson( + file.path.string(), + fls::Model::Invalid, + _scalingFactor + ); + } + } + catch (const ghoul::RuntimeError& e) { + LERRORC(e.component, e.message); + } +} + +void RenderableFieldlinesSequence::trackOldest(File& file) { + if (file.status == File::FileStatus::Loaded) { + std::deque::iterator it = + std::find(_loadedFiles.begin(), _loadedFiles.end(), &file); + if (it == _loadedFiles.end()) { + _loadedFiles.push_back(&file); + } + // Repopulate the queue if new File makes the queue full + if (!_loadedFiles.empty() && + _loadingType == LoadingType::DynamicDownloading && + _loadedFiles.size() >= _maxLoadedFiles) + { + File* oldest = _loadedFiles.front(); + // The edge case of when queue just got full and user jumped back to where + // they started which would make the oldes file in queue to be the active + // file. In that case we need to make sure we do not unload it + if (oldest == &file) { + return; + } + oldest->status = File::FileStatus::Downloaded; + oldest->state.clear(); + _loadedFiles.pop_front(); + } + } +} + +int RenderableFieldlinesSequence::updateActiveIndex(double currentTime) { + if (_files.empty()) { + return -1; + } + // Sets index to 0 if time is at the first file time stamp and also, + // if size == 1 then we can expect to not have a sequence and wants to show + // the one file of fieldlines at all points in time + if (_files.begin()->timestamp == currentTime || _files.size() == 1) { + return 0; + } + + const std::deque::const_iterator iter = std::upper_bound( + _files.begin(), + _files.end(), + currentTime, + [](double timeRef, const File& fileRef) { + return timeRef < fileRef.timestamp; + } + ); + + if (iter == _files.begin()) { + return 0; + } + else if (iter != _files.end()) { + return static_cast(std::distance(_files.cbegin(), iter)); + } + else { + return static_cast(_files.size()) - 1; } } @@ -964,10 +961,169 @@ bool RenderableFieldlinesSequence::isReady() const { return _shaderProgram != nullptr; } -void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks&) { - if (_activeTriggerTimeIndex == -1) { +void RenderableFieldlinesSequence::updateDynamicDownloading(double currentTime, + double deltaTime) +{ + _dynamicFileDownloader->update(currentTime, deltaTime); + const std::vector& filesToRead = + _dynamicFileDownloader->downloadedFiles(); + for (const std::filesystem::path& filePath : filesToRead) { + File newFile = { + .status = File::FileStatus::Downloaded, + .path = filePath, + .timestamp = extractTriggerTimeFromFilename(filePath.filename()) + }; + const std::deque::const_iterator iter = std::upper_bound( + _files.begin(), + _files.end(), + newFile.timestamp, + [](double timeRef, const File& fileRef) { + return timeRef < fileRef.timestamp; + } + ); + std::deque::iterator it = _files.insert(iter, std::move(newFile)); + trackOldest(*it); + } + + // If all files are moved into _sourceFiles then we can + // empty the DynamicFileSequenceDownloader's downloaded files list + _dynamicFileDownloader->clearDownloaded(); +} + +void RenderableFieldlinesSequence::firstUpdate() { + std::deque::iterator file = std::find_if( + _files.begin(), + _files.end(), + [](File& f) { return f.status == File::FileStatus::Loaded; } + ); + if (file == _files.end()) { return; } + + const std::vector>& quantities = file->state.extraQuantities(); + const std::vector& extraNamesVec = + file->state.extraQuantityNames(); + + for (int i = 0; i < quantities.size(); ++i) { + _colorQuantity.addOption(i, extraNamesVec[i]); + _maskingQuantity.addOption(i, extraNamesVec[i]); + } + _colorQuantity = _colorQuantityTemp; + _maskingQuantity = _maskingQuantityTemp; + + if (_colorTablePaths.size() > _colorQuantity) { + _colorTablePath = _colorTablePaths[_colorQuantity].string(); + } + else { + _colorTablePath = _colorTablePaths[0].string(); + } + + if (std::filesystem::exists(_colorTablePath.value())) { + _transferFunction = std::make_unique(_colorTablePath.value()); + } + else { + LWARNING("Invalid path to transfer function, please enter new path"); + _colorTablePath = FieldlinesSequenceModule::DefaultTransferFunctionFile.string(); + _transferFunction = + std::make_unique(_colorTablePath.stringValue()); + } + + _shouldUpdateColorBuffer = true; + _shouldUpdateMaskingBuffer = true; + + _isFirstLoad = false; +} + +void RenderableFieldlinesSequence::update(const UpdateData& data) { + if (_shaderProgram->isDirty()) { + _shaderProgram->rebuildFromFile(); + } + const double currentTime = data.time.j2000Seconds(); + const double deltaTime = global::timeManager->deltaTime(); + + if (_loadingType == LoadingType::DynamicDownloading) { + if (!_dynamicFileDownloader) { + const std::string& identifier = parent()->identifier(); + _dynamicFileDownloader = std::make_unique( + _dataID, + identifier, + _infoURL, + _dataURL, + _nFilesToQueue + ); + } + updateDynamicDownloading(currentTime, deltaTime); + computeSequenceEndTime(); + } + // At least one file in data set needs to be loaded to read extra variables + if (_isFirstLoad && _atLeastOneFileLoaded) { + firstUpdate(); + } + + _inInterval = !_files.empty() && + currentTime >= _files[0].timestamp && + currentTime < _sequenceEndTime; + + // For the sake of this if statment, it is easiest to think of activeIndex as the + // previous index and nextIndex as the current + const int nextIndex = _activeIndex + 1; + // if _activeIndex is -1 but we are in interval, it means we were before the start + // of the sequence in the previous frame + if (_activeIndex == -1 || + // if currentTime < active timestamp, it means that we stepped back to a + // time represented by another state + // _activeIndex has already been checked if it is <0 in the line above + currentTime < _files[_activeIndex].timestamp || + // if currentTime >= next timestamp, it means that we stepped forward to a + // time represented by another state + (nextIndex < _files.size() && currentTime >= _files[nextIndex].timestamp) || + // The case when we jumped passed last file. where nextIndex is not < file.size() + currentTime >= _files[_activeIndex].timestamp) + { + int previousIndex = _activeIndex; + _activeIndex = updateActiveIndex(currentTime); + // check index again after updating + if (_activeIndex == -1) { + return; + } + File& file = _files[_activeIndex]; + if (file.status == File::FileStatus::Downloaded) { + // if LoadingType is StaticLoading all files will be Loaded + // would be optimal if loading of next file would happen in the background + loadFile(file); + _isLoadingStateFromDisk = false; + file.status = File::FileStatus::Loaded; + file.timestamp = extractTriggerTimeFromFilename(file.path); + _atLeastOneFileLoaded = true; + computeSequenceEndTime(); + trackOldest(file); + } + // If we have a new index, buffers needs to update + if (previousIndex != _activeIndex) { + _shouldUpdateColorBuffer = true; + _shouldUpdateMaskingBuffer = true; + } + + updateVertexPositionBuffer(); + } + + if (_shouldUpdateColorBuffer) { + updateVertexColorBuffer(); + } + + if (_shouldUpdateMaskingBuffer) { + updateVertexMaskingBuffer(); + } +} + +void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks&) { + if (_files.empty() || _isFirstLoad) { + return; + } + if (!_inInterval && !_renderForever) { + return; + } + _shaderProgram->activate(); // Calculate Model View MatrixProjection @@ -978,8 +1134,10 @@ void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks& glm::dmat4(glm::scale(glm::dmat4(1), glm::dvec3(data.modelTransform.scale))); const glm::dmat4 modelViewMat = data.camera.combinedViewMatrix() * modelMat; - _shaderProgram->setUniform("modelViewProjection", - data.camera.sgctInternal.projectionMatrix() * glm::mat4(modelViewMat)); + _shaderProgram->setUniform( + "modelViewProjection", + data.camera.sgctInternal.projectionMatrix() * glm::mat4(modelViewMat) + ); _shaderProgram->setUniform("colorMethod", _colorMethod); _shaderProgram->setUniform("lineColor", _colorUniform); @@ -989,13 +1147,13 @@ void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks& if (_colorMethod == static_cast(ColorMethod::ByQuantity)) { ghoul::opengl::TextureUnit textureUnit; textureUnit.activate(); - _transferFunction->bind(); // Calls update internally - _shaderProgram->setUniform("colorTable", textureUnit); - _shaderProgram->setUniform("colorTableRange", _colorTableRanges[_colorQuantity]); + _transferFunction->bind(); + _shaderProgram->setUniform("transferFunction", textureUnit); + _shaderProgram->setUniform("selectedColorRange", _selectedColorRange); } if (_maskingEnabled) { - _shaderProgram->setUniform("maskingRange", _maskingRanges[_maskingQuantity]); + _shaderProgram->setUniform("maskingRange", _selectedMaskingRange); } _shaderProgram->setUniform("domainLimR", _domainR.value() * _scalingFactor); @@ -1003,7 +1161,7 @@ void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks& _shaderProgram->setUniform("domainLimY", _domainY.value() * _scalingFactor); _shaderProgram->setUniform("domainLimZ", _domainZ.value() * _scalingFactor); - // Flow/Particles + // Flow / Particles _shaderProgram->setUniform("flowColor", _flowColor); _shaderProgram->setUniform("usingParticles", _flowEnabled); _shaderProgram->setUniform("particleSize", _flowParticleSize); @@ -1030,11 +1188,24 @@ void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks& glLineWidth(1.f); #endif + int loadedIndex = _activeIndex; + if (loadedIndex == -1) { + return; + } + while (_files[loadedIndex].status != File::FileStatus::Loaded) { + --loadedIndex; + if (loadedIndex < 0) { + LWARNING("No file at or before current time is loaded"); + return; + } + } + + const FieldlinesState& state = _files[loadedIndex].state; glMultiDrawArrays( GL_LINE_STRIP, - _states[_activeStateIndex].lineStart().data(), - _states[_activeStateIndex].lineCount().data(), - static_cast(_states[_activeStateIndex].lineStart().size()) + state.lineStart().data(), + state.lineCount().data(), + static_cast(state.lineStart().size()) ); glBindVertexArray(0); @@ -1047,136 +1218,6 @@ void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks& } } -void RenderableFieldlinesSequence::update(const UpdateData& data) { - if (_shaderProgram->isDirty()) { - _shaderProgram->rebuildFromFile(); - } - // True if new 'runtime-state' must be loaded from disk. - // False => the previous frame's state should still be shown - bool mustLoadNewStateFromDisk = false; - // True if new 'in-RAM-state' must be loaded. - // False => the previous frame's state should still be shown - bool needUpdate = false; - const double currentTime = data.time.j2000Seconds(); - const bool isInInterval = (currentTime >= _startTimes[0]) && - (currentTime < _sequenceEndTime); - - // Check if current time in OpenSpace is within sequence interval - if (isInInterval) { - const size_t nextIdx = _activeTriggerTimeIndex + 1; - if ( - // true => Previous frame was not within the sequence interval - _activeTriggerTimeIndex < 0 || - // true => We stepped back to a time represented by another state - currentTime < _startTimes[_activeTriggerTimeIndex] || - // true => We stepped forward to a time represented by another state - (nextIdx < _nStates && currentTime >= _startTimes[nextIdx])) - { - updateActiveTriggerTimeIndex(currentTime); - - if (_loadingStatesDynamically) { - mustLoadNewStateFromDisk = true; - } - else { - needUpdate = true; - _activeStateIndex = _activeTriggerTimeIndex; - } - } // else {we're still in same state as previous frame (no changes needed)} - } - // if only one state - else if (_nStates == 1) { - _activeTriggerTimeIndex = 0; - _activeStateIndex = 0; - if (!_hasBeenUpdated) { - updateVertexPositionBuffer(); - } - - if (_states[_activeStateIndex].nExtraQuantities() > 0) { - _shouldUpdateColorBuffer = true; - _shouldUpdateMaskingBuffer = true; - } - - _hasBeenUpdated = true; - } - else { - // Not in interval => set everything to false - _activeTriggerTimeIndex = -1; - mustLoadNewStateFromDisk = false; - needUpdate = false; - } - - if (mustLoadNewStateFromDisk) { - if (!_isLoadingStateFromDisk && !_newStateIsReady) { - _isLoadingStateFromDisk = true; - mustLoadNewStateFromDisk = false; - std::string filePath = _sourceFiles[_activeTriggerTimeIndex]; - std::thread readBinaryThread([this, f = std::move(filePath)]() { - readNewState(f); - }); - readBinaryThread.detach(); - } - } - - if (needUpdate || _newStateIsReady) { - if (_loadingStatesDynamically) { - _states[0] = std::move(*_newState); - } - - updateVertexPositionBuffer(); - - if (_states[_activeStateIndex].nExtraQuantities() > 0) { - _shouldUpdateColorBuffer = true; - _shouldUpdateMaskingBuffer = true; - } - - // Everything is set and ready for rendering - needUpdate = false; - _newStateIsReady = false; - } - - if (_colorMethod == 1) { //By quantity - if (_shouldUpdateColorBuffer) { - updateVertexColorBuffer(); - _shouldUpdateColorBuffer = false; - } - - if (_shouldUpdateMaskingBuffer) { - updateVertexMaskingBuffer(); - _shouldUpdateMaskingBuffer = false; - } - } -} - -// Assumes we already know that currentTime is within the sequence interval -void RenderableFieldlinesSequence::updateActiveTriggerTimeIndex(double currentTime) { - auto iter = std::upper_bound(_startTimes.begin(), _startTimes.end(), currentTime); - if (iter != _startTimes.end()) { - if (iter != _startTimes.begin()) { - _activeTriggerTimeIndex = static_cast( - std::distance(_startTimes.begin(), iter) - ) - 1; - } - else { - _activeTriggerTimeIndex = 0; - } - } - else { - _activeTriggerTimeIndex = static_cast(_nStates) - 1; - } - if (_nStates == 1) { - _activeTriggerTimeIndex = 0; - } -} - -// Reading state from disk. Must be thread safe -void RenderableFieldlinesSequence::readNewState(const std::string& filePath) { - _newState = std::make_unique(); - if (_newState->loadStateFromOsfls(filePath)) { - _newStateIsReady = true; - } - _isLoadingStateFromDisk = false; -} - // Unbind buffers and arrays void unbindGL() { glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -1184,11 +1225,11 @@ void unbindGL() { } void RenderableFieldlinesSequence::updateVertexPositionBuffer() { - if (_activeStateIndex == -1) { return; } glBindVertexArray(_vertexArrayObject); glBindBuffer(GL_ARRAY_BUFFER, _vertexPositionBuffer); - const std::vector& vertPos = _states[_activeStateIndex].vertexPositions(); + const FieldlinesState& state = _files[_activeIndex].state; + const std::vector& vertPos = state.vertexPositions(); glBufferData( GL_ARRAY_BUFFER, @@ -1204,17 +1245,14 @@ void RenderableFieldlinesSequence::updateVertexPositionBuffer() { } void RenderableFieldlinesSequence::updateVertexColorBuffer() { - if (_activeStateIndex == -1) { return; } glBindVertexArray(_vertexArrayObject); glBindBuffer(GL_ARRAY_BUFFER, _vertexColorBuffer); - bool isSuccessful; - const std::vector& quantities = _states[_activeStateIndex].extraQuantity( - _colorQuantity, - isSuccessful - ); + const FieldlinesState& state = _files[_activeIndex].state; + bool success = false; + const std::vector& quantities = state.extraQuantity(_colorQuantity, success); - if (isSuccessful) { + if (success) { glBufferData( GL_ARRAY_BUFFER, quantities.size() * sizeof(float), @@ -1225,26 +1263,24 @@ void RenderableFieldlinesSequence::updateVertexColorBuffer() { glEnableVertexAttribArray(1); glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, nullptr); - unbindGL(); + _shouldUpdateColorBuffer = false; } + unbindGL(); } void RenderableFieldlinesSequence::updateVertexMaskingBuffer() { - if (_activeStateIndex == -1) { return; } glBindVertexArray(_vertexArrayObject); glBindBuffer(GL_ARRAY_BUFFER, _vertexMaskingBuffer); - bool isSuccessful; - const std::vector& maskings = _states[_activeStateIndex].extraQuantity( - _maskingQuantity, - isSuccessful - ); + const FieldlinesState& state = _files[_activeIndex].state; + bool success = false; + const std::vector& quantities = state.extraQuantity(_maskingQuantity, success); - if (isSuccessful) { + if (success) { glBufferData( GL_ARRAY_BUFFER, - maskings.size() * sizeof(float), - maskings.data(), + quantities.size() * sizeof(float), + quantities.data(), GL_STATIC_DRAW ); @@ -1252,6 +1288,7 @@ void RenderableFieldlinesSequence::updateVertexMaskingBuffer() { glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, nullptr); unbindGL(); + _shouldUpdateMaskingBuffer = false; } } diff --git a/modules/fieldlinessequence/rendering/renderablefieldlinessequence.h b/modules/fieldlinessequence/rendering/renderablefieldlinessequence.h index f5efbb4a53..5f69f45e83 100644 --- a/modules/fieldlinessequence/rendering/renderablefieldlinessequence.h +++ b/modules/fieldlinessequence/rendering/renderablefieldlinessequence.h @@ -35,12 +35,30 @@ #include #include #include -#include +#include +#include namespace openspace { class RenderableFieldlinesSequence : public Renderable { public: + enum class LoadingType { + StaticLoading, + DynamicDownloading + }; + + enum class SourceFileType { + Cdf, + Json, + Osfls + }; + + // Used to determine if lines should be colored uniformly or by some data variable + enum class ColorMethod { + Uniform, + ByQuantity + }; + explicit RenderableFieldlinesSequence(const ghoul::Dictionary& dictionary); void initialize() override; void initializeGL() override; @@ -50,84 +68,93 @@ public: void render(const RenderData& data, RendererTasks& rendererTask) override; void update(const UpdateData& data) override; + void updateDynamicDownloading(double currentTime, double deltaTime); + void firstUpdate(); + void computeSequenceEndTime(); static documentation::Documentation Documentation(); -private: - void addStateToSequence(FieldlinesState& STATE); - void computeSequenceEndTime(); - void definePropertyCallbackFunctions(); - void extractTriggerTimesFromFileNames(); - bool loadJsonStatesIntoRAM(); - void loadOsflsStatesIntoRAM(); - bool getStatesFromCdfFiles(); - void setModelDependentConstants(); - void setupProperties(); - bool prepareForOsflsStreaming(); + struct File { + enum class FileStatus { + Downloaded, + Loaded + }; + FileStatus status; + std::filesystem::path path; + // Assume timestamp is -1 until FileStatus is = Loaded + double timestamp = -1.0; + FieldlinesState state; - void readNewState(const std::string& filePath); - void updateActiveTriggerTimeIndex(double currentTime); + bool operator<(const File& other) const noexcept { + return timestamp < other.timestamp; + } + }; + +private: + void setupProperties(); + void setModelDependentConstants(); + + int updateActiveIndex(double currentTime); void updateVertexPositionBuffer(); void updateVertexColorBuffer(); void updateVertexMaskingBuffer(); - // Used to determine if lines should be colored UNIFORMLY or by an extraQuantity - enum class ColorMethod { - Uniform = 0, - ByQuantity = 1 - }; - enum class SourceFileType { - Cdf = 0, - Json = 1, - Osfls = 2 - }; + void staticallyLoadFiles(const std::optional& seed, + const std::optional& traceVariable); - // cdf, osfls or json - SourceFileType _inputFileType; - // Output folder path in case of file conversion - std::string _outputFolderPath; - // which tracing vaiable to trace. 'b' for fieldline is default - std::string _tracingVariable; + std::deque _files; + // The function loads the file in the sense that it creates the FieldlineState object + // in the File object. The function also deletes the oldest file if the loadedFiles + // queue is full. + void loadFile(File& file); + void trackOldest(File& file); + + // Not optional, but initialized with osfls because that is what we expect to be used + SourceFileType _inputFileType = SourceFileType::Osfls; + // Static Loading on default if not specified + LoadingType _loadingType = LoadingType::StaticLoading; // path to directory with seed point files std::filesystem::path _seedPointDirectory; - // optional except when using json input - std::string _modelStr; + // Which tracing variable to trace. Often 'b' is for magnetic field + std::string _tracingVariable; + // Extra variables such as rho, p or t for density, pressure or temperature + std::vector _extraVars; + float _manualTimeOffset = 0.0; + // Estimated / calculated end of sequence + double _sequenceEndTime = 0.0; + // If there's just one state it should never disappear + bool _renderForever = false; + bool _inInterval = false; - // Used for 'runtime-states'. True when loading a new state from disk on another - // thread. - std::atomic_bool _isLoadingStateFromDisk = false; - // False => states are stored in RAM (using 'in-RAM-states'), True => states are - // loaded from disk during runtime (using 'runtime-states') - bool _loadingStatesDynamically = false; - // Used for 'runtime-states'. True when finished loading a new state from disk on - // another thread. - std::atomic_bool _newStateIsReady = false; - // True when new state is loaded or user change which quantity to color the lines by - bool _shouldUpdateColorBuffer = false; - // True when new state is loaded or user change which quantity used for masking out - // line segments - bool _shouldUpdateMaskingBuffer = false; - // note Elon: rework the case of only one state - // hasBeenUpdated only gets sets once, first iteration of update function, to - // guarantee the vertext position buffer to be initialized. - bool _hasBeenUpdated = false; + // Data ID that corresponds to what dataset to use if using DynamicDownloading + int _dataID; + // Number of files to queue up at a time + size_t _nFilesToQueue = 10; + // To keep track of oldest file + std::deque _loadedFiles; + // The max number of files loaded at once + size_t _maxLoadedFiles = 100; + std::string _infoURL; + std::string _dataURL; + // DynamicFileSequenceDownloader downloads and updates renderable field lines with + // field lines downloaded from the web. + std::unique_ptr _dynamicFileDownloader; - // Active index of _states. If(==-1)=>no state available for current time. Always the - // same as _activeTriggerTimeIndex if(_loadingStatesDynamically==true), else - // always = 0 - int _activeStateIndex = -1; - // Active index of _startTimes - int _activeTriggerTimeIndex = -1; - // Manual time offset - double _manualTimeOffset = 0.0; - // Number of states in the sequence - size_t _nStates = 0; // In setup it is used to scale JSON coordinates. During runtime it is used to scale // domain limits. float _scalingFactor = 1.f; - // Estimated end of sequence. - // If there's just one state it should never disappear - double _sequenceEndTime = std::numeric_limits::max(); + fls::Model _model = fls::Model::Invalid; + bool _shouldUpdateMaskingBuffer = false; + bool _shouldUpdateColorBuffer = false; + int _activeIndex = -1; + bool _atLeastOneFileLoaded = false; + + bool _isLoadingStateFromDisk = false; + + std::unique_ptr _shaderProgram; + // Transfer function used to color lines when _pColorMethod is set to BY_QUANTITY + std::unique_ptr _transferFunction; + // OpenGL Vertex Array Object GLuint _vertexArrayObject = 0; // OpenGL Vertex Buffer Object containing the extraQuantity values used for coloring @@ -139,39 +166,20 @@ private: // OpenGL Vertex Buffer Object containing the vertex positions GLuint _vertexPositionBuffer = 0; - // The Lua-Modfile-Dictionary used during initialization - // Used for 'runtime-states' when switching out current state to a new state - std::unique_ptr _newState; - std::unique_ptr _shaderProgram; - // Transfer function used to color lines when _pColorMethod is set to BY_QUANTITY - std::unique_ptr _transferFunction; - - // Paths to color tables. One for each 'extraQuantity' - std::vector _colorTablePaths; - // Values represents min & max values represented in the color table - std::vector _colorTableRanges; - // Values represents min & max limits for valid masking range - std::vector _maskingRanges; - // Stores the provided source file paths if using 'runtime-states', else emptied after - // initialization - std::vector _sourceFiles; - // Extra variables such as rho, p or t - std::vector _extraVars; - // Contains the _triggerTimes for all FieldlineStates in the sequence - std::vector _startTimes; - // Stores the FieldlineStates - std::vector _states; - // Group to hold the color properties properties::PropertyOwner _colorGroup; // Uniform/transfer function/topology? properties::OptionProperty _colorMethod; // Index of the extra quantity to color lines by properties::OptionProperty _colorQuantity; - // Used to save property for later initialization - int _colorQuantityTemp = 0; - // Color table/transfer function min and max range - properties::Vec2Property _colorQuantityMinMax; + // Used to save property for later initialization, because firstUpdate needs to run + // first, to populate _colorQuantity with options + int _colorQuantityTemp; + std::vector _colorTableRanges; + // Color table/transfer function selected min and max range + properties::Vec2Property _selectedColorRange; + // Paths to color tables. One for each 'extraQuantity' + std::vector _colorTablePaths; // Color table/transfer function for "By Quantity" coloring properties::StringProperty _colorTablePath; // Uniform Field Line Color @@ -179,25 +187,21 @@ private: // Whether or not to use additive blending properties::BoolProperty _colorABlendEnabled; - // Whether or not to use Domain + // Whether or not to use Domain limits properties::BoolProperty _domainEnabled; // Group to hold the Domain properties properties::PropertyOwner _domainGroup; - // Domain Limits along x-axis properties::Vec2Property _domainX; - // Domain Limits along y-axis properties::Vec2Property _domainY; - // Domain Limits along z-axis properties::Vec2Property _domainZ; - // Domain Limits radially properties::Vec2Property _domainR; - // Simulated particles' color - properties::Vec4Property _flowColor; // Toggle flow [ON/OFF] properties::BoolProperty _flowEnabled; // Group to hold the flow/particle properties properties::PropertyOwner _flowGroup; + // Simulated particles' color + properties::Vec4Property _flowColor; // Size of simulated flow particles properties::IntProperty _flowParticleSize; // Size of simulated flow particles @@ -211,8 +215,9 @@ private: properties::BoolProperty _maskingEnabled; // Group to hold the masking properties properties::PropertyOwner _maskingGroup; - // Lower and upper range limit for allowed values - properties::Vec2Property _maskingMinMax; + std::vector _maskingRanges; + // Selected lower and upper range limits for masking + properties::Vec2Property _selectedMaskingRange; // Index of the extra quantity to use for masking properties::OptionProperty _maskingQuantity; // used to save property for later initialization @@ -221,7 +226,10 @@ private: // Line width for the line rendering part properties::FloatProperty _lineWidth; // Button which executes a time jump to start of sequence - properties::TriggerProperty _jumpToStartBtn; + properties::TriggerProperty _jumpToStart; + properties::BoolProperty _saveDownloadsOnShutdown; + + bool _isFirstLoad = true; }; } // namespace openspace diff --git a/modules/fieldlinessequence/shaders/fieldlinessequence_vs.glsl b/modules/fieldlinessequence/shaders/fieldlinessequence_vs.glsl index 6114bebe73..2fa560e4f8 100644 --- a/modules/fieldlinessequence/shaders/fieldlinessequence_vs.glsl +++ b/modules/fieldlinessequence/shaders/fieldlinessequence_vs.glsl @@ -39,8 +39,8 @@ uniform mat4 modelViewProjection; // Uniforms needed to color by quantity uniform int colorMethod; -uniform sampler1D colorTable; -uniform vec2 colorTableRange; +uniform sampler1D transferFunction; +uniform vec2 selectedColorRange; // Uniforms needed for Particle Flow uniform vec4 flowColor; @@ -69,8 +69,8 @@ const int colorByQuantity = 1; vec4 getTransferFunctionColor() { // Remap the color scalar to a [0,1] range float lookUpVal = - (in_color_scalar - colorTableRange.x) / (colorTableRange.y - colorTableRange.x); - return texture(colorTable, lookUpVal); + (in_color_scalar - selectedColorRange.x) / (selectedColorRange.y - selectedColorRange.x); + return texture(transferFunction, lookUpVal); } bool isPartOfParticle(double time, int vertexId, int particleSize, int particleSpeed, @@ -92,7 +92,7 @@ void main() { if (usingDomain && hasColor) { float radius = length(in_position); - + // If position is outside of domain if (in_position.x < domainLimX.x || in_position.x > domainLimX.y || in_position.y < domainLimY.x || in_position.y > domainLimY.y || in_position.z < domainLimZ.x || in_position.z > domainLimZ.y || diff --git a/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.cpp b/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.cpp new file mode 100644 index 0000000000..85434b1d7d --- /dev/null +++ b/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.cpp @@ -0,0 +1,204 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "KameleonVolumeToFieldlinesTask"; + + // This task class traces field lines from volume data. It takes a task file which + // specifies a folder with .cdf files, as well as a file that lists seed points from + // which the tracing starts. For the outputs, specify an `outputFolder` for where + // the field lines data will be saved and the `OutputType` parameter to be either + // `Osfls` which is an OpenSpace specific binary format for field lines, or `Json` for + // a readable version of the same data. Some knowledge of the data might be needed, + // especially if coloring the field lines according to some data parameter like + // temperature or density. These parameters needs to be specified in the `ScalarVars` + // and match the name in the input data. For the magnitude of a vector parameter, such + // as magnetic stength or velocity, there are specified in `MagntitudeVars`. + struct [[codegen::Dictionary(KameleonVolumeToFieldlinesTask)]] Parameters { + // The folder to the cdf files to extract data from. + std::filesystem::path input [[codegen::directory()]]; + + // Choose to decrease cadence and only use every nth time step / input file. + std::optional nthTimeStep; + + // A path to folder with text files with seedpoints. + // The format of points: x1 y1 z1 x2 y2 z2 ... + // Seedpoints are expressed in the native coordinate system of the model. + // Filename must match date and time for CDF file. + std::filesystem::path seedpoints [[codegen::directory()]]; + + // Choose to only include every nth seedpoint from each file. + std::optional nthSeedpoint; + + // If data sets parameter start_time differ from start of run, + // elapsed_time_in_seconds might be in relation to start of run. + // ManualTimeOffset will be added to trigger time. + std::optional manualTimeOffset; + + // The name of the kameleon variable to use for tracing, like b, or u. + std::string tracingVar; + + // The folder to write the files to. + std::filesystem::path outputFolder [[codegen::directory()]]; + + enum class [[codegen::map(openspace::KameleonVolumeToFieldlinesTask::OutputType)]] + OutputType { + Json, + Osfls + }; + // Output type + OutputType outputType; + + // A list of scalar variables to extract along the fieldlines + // like temperature or density. + std::optional> scalarVars; + + // A list of vector field variables. Must be in groups of 3, + // for example \"bx, by, bz\", \"ux, uy, uz\". + std::optional> magnitudeVars; + }; +#include "kameleonvolumetofieldlinestask_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation KameleonVolumeToFieldlinesTask::Documentation() { + return codegen::doc( + "fieldlinesequence_kameleon_volume_to_fieldlines_task" + ); +} + +KameleonVolumeToFieldlinesTask::KameleonVolumeToFieldlinesTask( + const ghoul::Dictionary& dictionary) +{ + const Parameters p = codegen::bake(dictionary); + + _inputPath = p.input; + _nthTimeStep = p.nthTimeStep.value_or(_nthTimeStep); + _seedpointsPath = p.seedpoints; + _nthSeedPoint = p.nthSeedpoint.value_or(_nthSeedPoint); + _manualTimeOffset = p.manualTimeOffset.value_or(_manualTimeOffset); + _outputFolder = p.outputFolder; + if (_outputFolder.string().back() != '/') { + _outputFolder += '/'; + } + _outputType = codegen::map(p.outputType); + _tracingVar = p.tracingVar; + + _scalarVars = p.scalarVars.value_or(std::vector()); + if (_scalarVars.empty()) { + LINFO("No scalar variable was specified to be saved"); + } + + _magnitudeVars = p.magnitudeVars.value_or(std::vector()); + if (_magnitudeVars.empty()) { + LINFO("No vector field variable was specified to be saved"); + } + + namespace fs = std::filesystem; + for (const fs::directory_entry& e : fs::directory_iterator(_inputPath)) { + if (e.is_regular_file()) { + std::string eStr = e.path().string(); + _sourceFiles.push_back(eStr); + } + } +} + +std::string KameleonVolumeToFieldlinesTask::description() { + return std::format( + "Extract fieldline data from cdf file {} and seedpoint file {}. " + "Write osfls file into the folder {}.", + _inputPath, _seedpointsPath, _outputFolder + ); +} + +void KameleonVolumeToFieldlinesTask::perform( + const Task::ProgressCallback& progressCallback) +{ + std::vector extraMagVars = + fls::extractMagnitudeVarsFromStrings(_magnitudeVars); + + std::unordered_map> seedPoints = + fls::extractSeedPointsFromFiles(_seedpointsPath, _nthSeedPoint); + if (seedPoints.empty()) { + throw ghoul::RuntimeError("Falied to read seedpoints"); + } + + size_t fileNumber = 0; + for (const std::string& cdfPath : _sourceFiles) { + if (fileNumber % _nthTimeStep != 0) { + continue; + } + + FieldlinesState newState; + bool isSuccessful = fls::convertCdfToFieldlinesState( + newState, + cdfPath, + seedPoints, + _manualTimeOffset, + _tracingVar, + _scalarVars, + extraMagVars + ); + if (isSuccessful) { + switch (_outputType) { + case OutputType::Osfls: + newState.saveStateToOsfls(_outputFolder.string()); + break; + case OutputType::Json: + { + std::string timeStr = + std::string(Time(newState.triggerTime()).ISO8601()); + timeStr.replace(13, 1, "-"); + timeStr.replace(16, 1, "-"); + timeStr.replace(19, 1, "-"); + std::string fileName = timeStr; + newState.saveStateToJson(_outputFolder.string() + fileName); + break; + } + default : + throw ghoul::MissingCaseException(); + } + } + ++fileNumber; + } + + // Ideally, we would want to signal about progress earlier as well, but + // convertCdfToFieldlinesState does all the work encapsulated in one function call. + progressCallback(1.f); +} + +} // namespace openspace diff --git a/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.h b/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.h new file mode 100644 index 0000000000..f0dbd70a8c --- /dev/null +++ b/modules/fieldlinessequence/tasks/kameleonvolumetofieldlinestask.h @@ -0,0 +1,64 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_FIELDLINESSEQUENCE___KAMELEONVOLUMETOFIELDLINESTASK___H__ +#define __OPENSPACE_MODULE_FIELDLINESSEQUENCE___KAMELEONVOLUMETOFIELDLINESTASK___H__ + +#include + +#include + +namespace openspace { + +class KameleonVolumeToFieldlinesTask : public Task { +public: + enum class OutputType { + Json, + Osfls + }; + + explicit KameleonVolumeToFieldlinesTask(const ghoul::Dictionary& dictionary); + + std::string description() override; + void perform(const Task::ProgressCallback& progressCallback) override; + static documentation::Documentation Documentation(); + +private: + std::string _tracingVar; + std::vector _scalarVars; + std::vector _magnitudeVars; + std::filesystem::path _inputPath; + size_t _nthTimeStep = 1; + std::vector _sourceFiles; + std::filesystem::path _seedpointsPath; + size_t _nthSeedPoint = 1; + OutputType _outputType; + std::filesystem::path _outputFolder; + // Manual time offset + float _manualTimeOffset = 0.f; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_FIELDLINESSEQUENCE___KAMELEONVOLUMETOFIELDLINESTASK___H__ diff --git a/modules/fieldlinessequence/util/fieldlinesstate.cpp b/modules/fieldlinessequence/util/fieldlinesstate.cpp index ad83414e3c..b3a4870e33 100644 --- a/modules/fieldlinessequence/util/fieldlinesstate.cpp +++ b/modules/fieldlinessequence/util/fieldlinesstate.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -61,10 +62,25 @@ void FieldlinesState::scalePositions(float scale) { } } +FieldlinesState FieldlinesState::createStateFromOsfls(const std::string& path) { + FieldlinesState s; + const bool success = s.loadStateFromOsfls(path); + if (!success) { + throw ghoul::RuntimeError(std::format( + "Failed to load state from osfls file {}", path + )); + } + return s; +} + bool FieldlinesState::loadStateFromOsfls(const std::string& pathToOsflsFile) { - std::ifstream ifs(pathToOsflsFile, std::ifstream::binary); + std::ifstream ifs = std::ifstream(pathToOsflsFile, std::ifstream::binary); + if (!ifs.is_open()) { - LERROR("Couldn't open file: " + pathToOsflsFile); + LERROR(std::format( + "Could not open file: {}. Error code: {}", + pathToOsflsFile, std::strerror(errno) + )); return false; } @@ -89,7 +105,7 @@ bool FieldlinesState::loadStateFromOsfls(const std::string& pathToOsflsFile) { // Read single value variables ifs.read(reinterpret_cast(&_triggerTime), sizeof(double)); ifs.read(reinterpret_cast(&_model), sizeof(int32_t)); - ifs.read(reinterpret_cast(&_isMorphable), sizeof(bool)); + ifs.read(reinterpret_cast(&_isMorphable), sizeof(uint8_t)); ifs.read(reinterpret_cast(&nLines), sizeof(uint64_t)); ifs.read(reinterpret_cast(&nPoints), sizeof(uint64_t)); ifs.read(reinterpret_cast(&nExtras), sizeof(uint64_t)); @@ -226,7 +242,8 @@ bool FieldlinesState::loadStateFromJson(const std::string& pathToJsonFile, * CurrentVersion) * 1. double - _triggerTime * 2. int - _model - * 3. bool - _isMorphable + * 3. uint8_t works like bool- _isMorphable + * 0 for false, 1 for true * 4. size_t - Number of lines in the state == _lineStart.size() * == _lineCount.size() * 5. size_t - Total number of vertex points == _vertexPositions.size() @@ -277,7 +294,7 @@ void FieldlinesState::saveStateToOsfls(const std::string& absPath) { //-------------------- WRITE META DATA FOR STATE -------------------------------- ofs.write(reinterpret_cast(&_triggerTime), sizeof(_triggerTime)); ofs.write(reinterpret_cast(&_model), sizeof(int32_t)); - ofs.write(reinterpret_cast(&_isMorphable), sizeof(bool)); + ofs.write(reinterpret_cast(&_isMorphable), sizeof(uint8_t)); ofs.write(reinterpret_cast(&nLines), sizeof(uint64_t)); ofs.write(reinterpret_cast(&nPoints), sizeof(uint64_t)); @@ -296,6 +313,8 @@ void FieldlinesState::saveStateToOsfls(const std::string& absPath) { ofs.write(reinterpret_cast(vec.data()), sizeof(float) * nPoints); } ofs.write(allExtraQuantityNamesInOne.c_str(), nStringBytes); + + LINFO(std::format("Saving fieldline state to: {}", absPath)); } // TODO: This should probably be rewritten, but this is the way the files were structured @@ -375,6 +394,18 @@ void FieldlinesState::saveStateToJson(const std::string& absPath) { LINFO(std::format("Saved fieldline state to: {}{}", absPath, ext)); } +void FieldlinesState::clear() { + _isMorphable = 0; + _triggerTime = -1.0; + _model = fls::Model::Invalid; + + _extraQuantities.clear(); + _extraQuantityNames.clear(); + _lineCount.clear(); + _lineStart.clear(); + _vertexPositions.clear(); +} + void FieldlinesState::setModel(fls::Model m) { _model = m; } @@ -387,6 +418,10 @@ void FieldlinesState::setTriggerTime(double t) { // If index is out of scope an empty vector is returned and the referenced bool is false. std::vector FieldlinesState::extraQuantity(size_t index, bool& isSuccessful) const { + if (index == -1) { + isSuccessful = false; + return std::vector(); + } if (index < _extraQuantities.size()) { isSuccessful = true; return _extraQuantities[index]; @@ -394,13 +429,12 @@ std::vector FieldlinesState::extraQuantity(size_t index, bool& isSuccessf else { isSuccessful = false; LERROR("Provided Index was out of scope"); - return {}; + return std::vector(); } } // Moves the points in @param line over to _vertexPositions and updates // _lineStart & _lineCount accordingly. - void FieldlinesState::addLine(std::vector& line) { const size_t nNewPoints = line.size(); const size_t nOldPoints = _vertexPositions.size(); diff --git a/modules/fieldlinessequence/util/fieldlinesstate.h b/modules/fieldlinessequence/util/fieldlinesstate.h index 062cb68e8c..3b852bf44e 100644 --- a/modules/fieldlinessequence/util/fieldlinesstate.h +++ b/modules/fieldlinessequence/util/fieldlinesstate.h @@ -35,6 +35,10 @@ namespace openspace { class FieldlinesState { public: + static FieldlinesState createStateFromOsfls(const std::string& pathToSoflsFile); + + FieldlinesState() = default; + void convertLatLonToCartesian(float scale = 1.f); void scalePositions(float scale); @@ -45,6 +49,8 @@ public: float coordToMeters); void saveStateToJson(const std::string& pathToJsonFile); + void clear(); + const std::vector>& extraQuantities() const; const std::vector& extraQuantityNames() const; const std::vector& lineCount() const; @@ -66,9 +72,9 @@ public: void appendToExtra(size_t idx, float val); private: - bool _isMorphable = false; + uint8_t _isMorphable = 0; double _triggerTime = -1.0; - fls::Model _model; + fls::Model _model = fls::Model::Invalid; std::vector> _extraQuantities; std::vector _extraQuantityNames; diff --git a/modules/fieldlinessequence/util/kameleonfieldlinehelper.cpp b/modules/fieldlinessequence/util/kameleonfieldlinehelper.cpp index c15eafbd86..92ff3c22c0 100644 --- a/modules/fieldlinessequence/util/kameleonfieldlinehelper.cpp +++ b/modules/fieldlinessequence/util/kameleonfieldlinehelper.cpp @@ -29,7 +29,9 @@ #include #include #include +#include #include +#include #ifdef OPENSPACE_MODULE_KAMELEON_ENABLED @@ -126,6 +128,101 @@ bool convertCdfToFieldlinesState(FieldlinesState& state, const std::string& cdfP #endif // OPENSPACE_MODULE_KAMELEON_ENABLED } +std::unordered_map> +extractSeedPointsFromFiles(std::filesystem::path path, size_t nth) +{ + std::unordered_map> outMap; + + if (!std::filesystem::is_directory(path)) { + throw ghoul::RuntimeError(std::format( + "The specified seed point directory: '{}' does not exist", path + )); + return outMap; + } + + namespace fs = std::filesystem; + for (const fs::directory_entry& spFile : fs::directory_iterator(path)) { + fs::path seedFilePath = spFile.path(); + if (!spFile.is_regular_file() || seedFilePath.extension() != "txt") { + continue; + } + + std::ifstream seedFile = std::ifstream(seedFilePath); + if (!seedFile.good()) { + LERROR(std::format("Could not open seed points file '{}'", seedFilePath)); + outMap.clear(); + return {}; + } + + LDEBUG(std::format("Reading seed points from file '{}'", seedFilePath)); + std::string line; + std::vector outVec; + int linenumber = 0; + while (ghoul::getline(seedFile, line)) { + if (linenumber % nth == 0) { + if (!line.empty() && line[0] == '#') { + // Ignore line, assume it's a comment + continue; + } + std::stringstream ss(line); + glm::vec3 point; + if (!(ss >> point.x) || !(ss >> point.y) || !(ss >> point.z)) { + LERROR(std::format( + "Could not read line '{}' in file '{}'. Line is not formatted " + "with 3 values representing a point", + linenumber, seedFilePath + )); + } + else { + outVec.push_back(std::move(point)); + } + } + ++linenumber; + } + + if (outVec.empty()) { + LERROR(std::format("Found no seed points in: {}", seedFilePath)); + return {}; + } + + std::string name = seedFilePath.stem().string(); // remove file extention + size_t dateAndTimeSeperator = name.find_last_of('_'); + std::string time = name.substr(dateAndTimeSeperator + 1, name.length()); + std::string date = name.substr(dateAndTimeSeperator - 8, 8); // 8 for yyyymmdd + std::string dateAndTime = date + time; + + // Add outVec as value, with time stamp as key + outMap[dateAndTime] = outVec; + } + return outMap; +} +std::vector +extractMagnitudeVarsFromStrings(std::vector vars) +{ + std::vector extraMagVars; + for (size_t i = 0; i < vars.size(); i++) { + const std::string& str = vars[i]; + + std::istringstream ss(str); + std::string magVar; + size_t counter = 0; + while (ghoul::getline(ss, magVar, ',')) { + std::erase_if(magVar, ::isspace); + extraMagVars.push_back(magVar); + counter++; + if (counter == 3) { + break; + } + } + if (counter != 3 && counter > 0) { + extraMagVars.erase(extraMagVars.end() - counter, extraMagVars.end()); + } + vars.erase(vars.begin() + i); + i--; + } + return extraMagVars; +} + #ifdef OPENSPACE_MODULE_KAMELEON_ENABLED /** * Traces and adds line vertices to state. @@ -140,10 +237,10 @@ bool addLinesToState(ccmc::Kameleon* kameleon, const std::vector& see switch (state.model()) { case fls::Model::Batsrus: - innerBoundaryLimit = 2.5f; // TODO specify in Lua? + innerBoundaryLimit = 0.f; // TODO (2025-06-10 Elon) specify in Lua? break; case fls::Model::Enlil: - innerBoundaryLimit = 0.11f; // TODO specify in Lua? + innerBoundaryLimit = 0.11f; break; default: LERROR( @@ -169,7 +266,7 @@ bool addLinesToState(ccmc::Kameleon* kameleon, const std::vector& see //--------------------------------------------------------------------------// auto interpolator = std::make_unique(kameleon->model); ccmc::Tracer tracer(kameleon, interpolator.get()); - tracer.setInnerBoundary(innerBoundaryLimit); // TODO specify in Lua? + tracer.setInnerBoundary(innerBoundaryLimit); ccmc::Fieldline ccmcFieldline = tracer.bidirectionalTrace( tracingVar, seed.x, @@ -178,15 +275,21 @@ bool addLinesToState(ccmc::Kameleon* kameleon, const std::vector& see ); const std::vector& positions = ccmcFieldline.getPositions(); - const size_t nLinePoints = positions.size(); + const ccmc::Point3f& firstPos = positions[0]; + const ccmc::Point3f& lastPos = positions[positions.size() - 1]; + if ((firstPos.component3 < 0.5f && firstPos.component3 > -0.5f) && + (lastPos.component3 < 0.5f && lastPos.component3 > -0.5f)) + { + const size_t nLinePoints = positions.size(); - std::vector vertices; - vertices.reserve(nLinePoints); - for (const ccmc::Point3f& p : positions) { - vertices.emplace_back(p.component1, p.component2, p.component3); + std::vector vertices; + vertices.reserve(nLinePoints); + for (const ccmc::Point3f& p : positions) { + vertices.emplace_back(p.component1, p.component2, p.component3); + } + state.addLine(vertices); + success |= (nLinePoints > 0); } - state.addLine(vertices); - success |= (nLinePoints > 0); } return success; @@ -214,6 +317,18 @@ void addExtraQuantities(ccmc::Kameleon* kameleon, std::vector& extraMagVars, FieldlinesState& state) { + int nVariableAttributes = kameleon->getNumberOfVariableAttributes(); + std::vector variablesAttributeNames; + for (int i = 0; i < nVariableAttributes; ++i) { + std::string varname = kameleon->getVariableAttributeName(i); + std::string varunit = kameleon->getNativeUnit(varname); + std::string varVisualizationUnit = kameleon->getVisUnit(varname); + LINFO(std::format( + "Variable '{}' is : '{}'. Variable Unit: '{}'. Visualization unit: '{}'.", + i, varname, varunit, varVisualizationUnit + )); + variablesAttributeNames.push_back(varname); + } prepareStateAndKameleonForExtras(kameleon, extraScalarVars, extraMagVars, state); diff --git a/modules/fieldlinessequence/util/kameleonfieldlinehelper.h b/modules/fieldlinessequence/util/kameleonfieldlinehelper.h index 2839060a98..330786c79c 100644 --- a/modules/fieldlinessequence/util/kameleonfieldlinehelper.h +++ b/modules/fieldlinessequence/util/kameleonfieldlinehelper.h @@ -35,7 +35,20 @@ namespace openspace { class FieldlinesState; namespace fls { +std::vector + extractMagnitudeVarsFromStrings(std::vector extrVars); +/** + * Extract seedpoints from a text file. This function is used both in + * RenderableFieldlinesSequence and the .cdf to .osfls converter task. + * + * \param path The path to a directory with files containing list of seedpoints + * \param nth Is 1 on default to incluse every seedpoint. nth can be used to reduce the + amount of data produced be only including every nth seed point + * \return A list of seedpoints, mapped by their corresponding time step + */ +std::unordered_map> extractSeedPointsFromFiles( + std::filesystem::path path, size_t nth = 1); /** * Traces field lines from the provided cdf file using kameleon and stores the data in the * provided FieldlinesState. Returns `false` if it fails to create a valid state. Requires diff --git a/modules/fitsfilereader/CMakeLists.txt b/modules/fitsfilereader/CMakeLists.txt index b13c2b83d3..f1d0ffdc5e 100644 --- a/modules/fitsfilereader/CMakeLists.txt +++ b/modules/fitsfilereader/CMakeLists.txt @@ -27,12 +27,16 @@ include(${PROJECT_SOURCE_DIR}/support/cmake/module_definition.cmake) set(HEADER_FILES fitsfilereadermodule.h include/fitsfilereader.h + include/renderabletimevaryingfitssphere.h + include/wsafitshelper.h ) source_group("Header Files" FILES ${HEADER_FILES}) set(SOURCE_FILES fitsfilereadermodule.cpp src/fitsfilereader.cpp + src/renderabletimevaryingfitssphere.cpp + src/wsafitshelper.cpp ) source_group("Source Files" FILES ${SOURCE_FILES}) diff --git a/modules/fitsfilereader/fitsfilereadermodule.cpp b/modules/fitsfilereader/fitsfilereadermodule.cpp index c17f247e36..78436d37c6 100644 --- a/modules/fitsfilereader/fitsfilereadermodule.cpp +++ b/modules/fitsfilereader/fitsfilereadermodule.cpp @@ -24,8 +24,29 @@ #include +#include +#include +#include +#include + namespace openspace { FitsFileReaderModule::FitsFileReaderModule() : OpenSpaceModule(Name) {} +void FitsFileReaderModule::internalInitialize(const ghoul::Dictionary&) { + ghoul::TemplateFactory* fRenderable = + FactoryManager::ref().factory(); + ghoul_assert(fRenderable, "No renderable factory existed"); + fRenderable->registerClass( + "RenderableTimeVaryingFitsSphere" + ); +} + +std::vector FitsFileReaderModule::documentations() const { + return { + FitsFileReaderModule::Documentation(), + RenderableTimeVaryingFitsSphere::Documentation() + }; +} + } // namespace openspace diff --git a/modules/fitsfilereader/fitsfilereadermodule.h b/modules/fitsfilereader/fitsfilereadermodule.h index 1fdd480f7d..cd5e921dd0 100644 --- a/modules/fitsfilereader/fitsfilereadermodule.h +++ b/modules/fitsfilereader/fitsfilereadermodule.h @@ -34,6 +34,11 @@ public: constexpr static const char* Name = "FitsFileReader"; FitsFileReaderModule(); + + std::vector documentations() const override; + +private: + void internalInitialize(const ghoul::Dictionary&) override; }; } // namespace openspace diff --git a/modules/fitsfilereader/include/renderabletimevaryingfitssphere.h b/modules/fitsfilereader/include/renderabletimevaryingfitssphere.h new file mode 100644 index 0000000000..6912882005 --- /dev/null +++ b/modules/fitsfilereader/include/renderabletimevaryingfitssphere.h @@ -0,0 +1,127 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_FITSFILEREADER___RENDERABLETIMEVARYINGFITSSPHERE___H__ +#define __OPENSPACE_MODULE_FITSFILEREADER___RENDERABLETIMEVARYINGFITSSPHERE___H__ + +#include + +#include +#include + +namespace openspace { + +struct RenderData; +struct UpdateData; +namespace documentation { struct Documentation; } + +class RenderableTimeVaryingFitsSphere : public RenderableSphere { +public: + enum class LoadingType { + StaticLoading, + DynamicDownloading + }; + + explicit RenderableTimeVaryingFitsSphere(const ghoul::Dictionary& dictionary); + + void deinitializeGL() override; + + void update(const UpdateData& data) override; + void render(const RenderData& data, RendererTasks& rendererTask) override; + + static documentation::Documentation Documentation(); + +protected: + void bindTexture() override; + +private: + struct File { + enum class FileStatus { + Downloaded, + Loaded + }; + FileStatus status = FileStatus::Downloaded; + std::filesystem::path path; + double time = 0.0; + std::unique_ptr texture; + glm::vec2 dataMinMax = { 0.f, 1.f }; + bool operator<(const File& other) const noexcept { + return time < other.time; + } + }; + void loadTexture(); + void trackOldest(File& file); + void showCorrectFileName(); + void extractMandatoryInfoFromSourceFolder(); + void readFileFromFits(std::filesystem::path path); + glm::vec2 minMaxTextureDataValues(std::unique_ptr& t); + void updateActiveTriggerTimeIndex(double currenttime); + void computeSequenceEndTime(); + void updateDynamicDownloading(double currentTime, double deltaTime); + + properties::OptionProperty _fitsLayerName; + // An option to keep or delete the downloads from dynamic downloader on shutdown + // Deletes on default + properties::BoolProperty _saveDownloadsOnShutdown; + properties::OptionProperty _textureFilterProperty; + properties::StringProperty _textureSource; + + // If there's just one state it should never disappear! + double _sequenceEndTime = std::numeric_limits::max(); + // Static Loading on default / if not specified + LoadingType _loadingType = LoadingType::StaticLoading; + // A data ID that corresponds to what dataset to use if using DynamicDownloading + int _dataID = -1; + // Number of files to queue up at a time + int _nFilesToQueue = 10; + // To keep track of oldest file + std::deque _loadedFiles; + // Max number of files loaded at once + size_t _maxLoadedFiles = 100; + std::string _infoUrl; + std::string _dataUrl; + std::map _layerNames; + std::map> _layerMinMaxCaps; + + int _fitsLayerTemp = -1; + // If there's just one state it should never disappear + bool _renderForever = false; + bool _inInterval = false; + + bool _isLoadingStateFromDisk = false; + // DynamicFileSequenceDownloader downloads and updates the renderable with + // data downloaded from the web + std::unique_ptr _dynamicFileDownloader; + std::deque _files; + int _activeTriggerTimeIndex = 0; + + bool _firstUpdate = true; + bool _layerOptionsAdded = false; + ghoul::opengl::Texture* _texture = nullptr; + bool _textureIsDirty = true; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_FITSFILEREADER___RENDERABLETIMEVARYINGFITSSPHERE___H__ diff --git a/modules/fitsfilereader/include/wsafitshelper.h b/modules/fitsfilereader/include/wsafitshelper.h new file mode 100644 index 0000000000..6b49cbf379 --- /dev/null +++ b/modules/fitsfilereader/include/wsafitshelper.h @@ -0,0 +1,75 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_FITSFILEREADER___WSAFITSHELPER___H__ +#define __OPENSPACE_MODULE_FITSFILEREADER___WSAFITSHELPER___H__ + +#include +#include +#include + +namespace CCfits { + class FITS; + class PHDU; + class ExtHDU; +} // namespace CCfits + +namespace openspace { + +template +struct ImageData { + std::valarray contents; + int width; + int height; +}; + +/** + * Load image from a FITS file into a texture. + * + * \param path The path to the FITS file + * \param layerIndex The index of the layer to load from the FITS file + * \param minMax The minimum and maximum value range in which to cap the data between + Values outside of range will be overexposed + \return The texture created from the layer in the file with the set min-max range + */ +std::unique_ptr loadTextureFromFits( + const std::filesystem::path& path, size_t layerIndex, + const std::pair& minMax); + +void readFitsHeader(const std::filesystem::path& path); + +/** + * Get the number of data layers in a FITS file. + * + * \param path The path to the FITS file + * \return The number of layers in the FITS file + */ +int nLayers(const std::filesystem::path& path); + +template +std::shared_ptr> readImageInternal(U& image); + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_FITSFILEREADER___WSAFITSHELPER___H__ diff --git a/modules/fitsfilereader/src/renderabletimevaryingfitssphere.cpp b/modules/fitsfilereader/src/renderabletimevaryingfitssphere.cpp new file mode 100644 index 0000000000..77c20ad77d --- /dev/null +++ b/modules/fitsfilereader/src/renderabletimevaryingfitssphere.cpp @@ -0,0 +1,678 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "RenderableTimeVaryingFitsSphere"; + + // Extract J2000 time from file names + // Requires file to be named as example : 'wsa_202209291400R011_agong.fits' + // Looks for timestamp after first '_' + double extractTriggerTimeFromFitsFileName(const std::filesystem::path& filePath) { + // Extract the filename from the path (without extension) + std::string fileName = filePath.stem().string(); + + std::string digits; + bool foundDigits = false; + + // Iterate over the characters in the file name + for (char c : fileName) { + if (std::isdigit(c)) { + // If current character is a digit, append it to digits string + digits += c; + foundDigits = true; + } + else { + // If current character is not a digit, reset digits string + digits.clear(); + foundDigits = false; + } + + // If we have found at least 12 consecutive digits, break the loop + if (digits.size() >= 12) { + break; + } + } + // If no digits found, return an empty string + if (!foundDigits || digits.size() < 12) { + return -1; + } + + // Extract digits from the substring and construct ISO 8601 formatted string + std::ostringstream oss; + oss << digits.substr(0, 4) << "-" // Year + << digits.substr(4, 2) << "-" // Month + << digits.substr(6, 2) << "T" // Day + << digits.substr(8, 2) << ":" // Hour + << digits.substr(10, 2) << ":" // Minute + << "00" + << digits.substr(12, 2) << "." // Second + << "000"; + + return openspace::Time::convertTime(oss.str()); + } + + constexpr openspace::properties::Property::PropertyInfo TextureSourceInfo = { + "TextureSource", + "Texture Source", + "A directory on disk from which to load the texture files for the sphere.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo FitsLayerInfo = { + "FitsLayer", + "Texture Layer", + "The index of the layer in the FITS file to use as texture.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo FitsLayerNameInfo = { + "LayerNames", + "Texture Layer Options", + "This value specifies which name of the fits layer to use as texture.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo TextureFilterInfo = { + "TextureFilter", + "Texture Filter", + "Option to choose nearest neighbor or linear filtering for the texture.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo SaveDownloadsOnShutdown = { + "SaveDownloadsOnShutdown", + "Save Downloads On Shutdown", + "This is an option for if dynamically downloaded files should be saved for the" + "next run or not.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + struct [[codegen::Dictionary(RenderableTimeVaryingFitsSphere)]] Parameters { + // [[codegen::verbatim(TextureSourceInfo.description)]] + std::optional textureSource; + + enum class [[codegen::map( + openspace::RenderableTimeVaryingFitsSphere::LoadingType)]] LoadingType + { + StaticLoading, + DynamicDownloading + }; + + // Choose type of loading: + // StaticLoading: Download and load files on startup. + // DynamicDownloading: Download and load files during run time. + std::optional loadingType; + + // A data ID that corresponds to what dataset to use if using dynamicWebContent. + std::optional dataID; + + // This is a max value of the amount of files to queue up + // so that not to big of a data set is downloaded. + std::optional numberOfFilesToQueue; + + // A URL that returns a JSON formated page with metadata needed for the dataURL. + std::optional infoURL; + + // A URL that returns a JSON formated page with a list of each available file. + std::optional dataURL; + + // [[codegen::verbatim(FitsLayerInfo.description)]] + std::optional fitsLayer; + + // [[codegen::verbatim(FitsLayerNameInfo.description)]] + std::optional layerNames; + + // A range per layer to be used to cap where the color range will lie. + // Values outside of range will be overexposed, i.e. data values below the min + // or above the max, will all be set to the min and max color value in range. + std::optional layerMinMaxCapValues; + + // This is set to false by default and will delete all the downloaded content when + // OpenSpace is shut down. Set to true to save all the downloaded files. + std::optional cacheData; + + // Set if first/last file should render outside of the sequence interval. + std::optional showPastFirstAndLastFile; + }; +#include "renderabletimevaryingfitssphere_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation RenderableTimeVaryingFitsSphere::Documentation() { + return codegen::doc( + "fitsfilereader_renderable_time_varying_fits_sphere", + RenderableSphere::Documentation() + ); +} + +RenderableTimeVaryingFitsSphere::RenderableTimeVaryingFitsSphere( + const ghoul::Dictionary& dictionary) + : RenderableSphere(dictionary) + , _fitsLayerName(FitsLayerNameInfo) + , _saveDownloadsOnShutdown(SaveDownloadsOnShutdown, false) + , _textureFilterProperty(TextureFilterInfo) + , _textureSource(TextureSourceInfo) +{ + const Parameters p = codegen::bake(dictionary); + + if (p.textureSource.has_value()) { + _textureSource = p.textureSource->string(); + } + + _textureSource.setReadOnly(true); + addProperty(_textureSource); // Added only to show in GUI which file is active + + if (p.loadingType.has_value()) { + _loadingType = codegen::map(*p.loadingType); + } + else { + _loadingType = LoadingType::StaticLoading; + } + + if (p.fitsLayer.has_value()) { + _fitsLayerTemp = *p.fitsLayer; + } + + _textureFilterProperty.addOptions({ + { static_cast(ghoul::opengl::Texture::FilterMode::Nearest), "No Filter" }, + { + static_cast(ghoul::opengl::Texture::FilterMode::Linear), + "Linear smoothing" + } + }); + _textureFilterProperty = static_cast( + ghoul::opengl::Texture::FilterMode::Nearest + ); + + _renderForever = p.showPastFirstAndLastFile.value_or(_renderForever); + + _textureFilterProperty.onChange([this]() { + switch (_textureFilterProperty) { + case static_cast(ghoul::opengl::Texture::FilterMode::Nearest): + for (File& file : _files) { + if (file.texture) { + file.texture->setFilter( + ghoul::opengl::Texture::FilterMode::Nearest + ); + } + } + break; + case static_cast(ghoul::opengl::Texture::FilterMode::Linear): + for (File& file : _files) { + if (file.texture) { + file.texture->setFilter( + ghoul::opengl::Texture::FilterMode::Linear + ); + } + } + break; + } + }); + addProperty(_textureFilterProperty); + + _fitsLayerName.onChange([this]() { + if (_loadingType == LoadingType::StaticLoading) { + extractMandatoryInfoFromSourceFolder(); + } + else { + for (File& file : _files) { + // This if-statement might seem redundant since we're loading a texture + // here, but the files without texture should not be loaded to save memory + if (file.status == File::FileStatus::Loaded) { + std::pair minMax = _layerMinMaxCaps.at(_fitsLayerName); + file.texture = loadTextureFromFits(file.path, _fitsLayerName, minMax); + file.texture->uploadTexture(); + using FM = ghoul::opengl::Texture::FilterMode; + if (_textureFilterProperty == static_cast(FM::Nearest)) { + file.texture->setFilter(FM::Nearest); + } + else if (_textureFilterProperty == static_cast(FM::Linear)) { + file.texture->setFilter(FM::Linear); + } + } + } + loadTexture(); + } + }); + addProperty(_fitsLayerName); + + if (_loadingType == LoadingType::StaticLoading) { + extractMandatoryInfoFromSourceFolder(); + computeSequenceEndTime(); + loadTexture(); + } + + if (_loadingType == LoadingType::DynamicDownloading) { + if (!p.dataID.has_value()) { + throw ghoul::RuntimeError( + "If running with dynamic downloading, dataID needs to be specified" + ); + } + _dataID = *p.dataID; + + _nFilesToQueue = p.numberOfFilesToQueue.value_or(_nFilesToQueue); + + if (!p.infoURL.has_value()) { + throw ghoul::RuntimeError( + "If running with dynamic downloading, infoURL needs to be specified" + ); + } + _infoUrl = *p.infoURL; + + if (!p.dataURL.has_value()) { + throw ghoul::RuntimeError( + "If running with dynamic downloading, dataURL needs to be specified" + ); + } + _dataUrl = *p.dataURL; + + if (p.layerMinMaxCapValues.has_value()) { + const ghoul::Dictionary d = *p.layerMinMaxCapValues; + for (std::string_view intKey : d.keys()) { + const ghoul::Dictionary& pair = d.value(intKey); + if (pair.hasValue("1") && pair.hasValue("2")) { + std::pair minMax = std::pair( + static_cast(pair.value("1")), + static_cast(pair.value("2")) + ); + std::pair> mapPair = std::pair( + std::stoi(std::string(intKey)), + minMax + ); + _layerMinMaxCaps.emplace(mapPair); + } + else { + throw ghoul::RuntimeError(std::format( + "The two values at {} needs to be of type double, a number with " + "at least one decimal", + intKey + )); + } + } + } + + if (!p.layerNames.has_value()) { + throw ghoul::RuntimeError("At least one name for one layer is required"); + } + const ghoul::Dictionary names = *p.layerNames; + for (std::string_view key : names.keys()) { + const int k = std::stoi(std::string(key)); + std::string v = names.value(key); + _layerNames.emplace(k, std::move(v)); + } + if (!p.fitsLayer.has_value()) { + LWARNING(std::format( + "Specify 'FitsLayer' for scene graph node with DataID: {}. ", + "Assuming first layer", + _dataID + )); + _fitsLayerTemp = 0; + } + } + _saveDownloadsOnShutdown = p.cacheData.value_or(_saveDownloadsOnShutdown); + addProperty(_saveDownloadsOnShutdown); +} + +void RenderableTimeVaryingFitsSphere::deinitializeGL() { + if (_loadingType == LoadingType::DynamicDownloading && _dynamicFileDownloader) { + _dynamicFileDownloader->deinitialize(_saveDownloadsOnShutdown); + } + _texture = nullptr; + _files.clear(); + RenderableSphere::deinitializeGL(); +} + +void RenderableTimeVaryingFitsSphere::readFileFromFits(std::filesystem::path path) { + if (!_layerOptionsAdded) { + for (const std::pair& name : _layerNames) { + _fitsLayerName.addOption(name.first, name.second); + } + _fitsLayerName = _fitsLayerTemp; + _layerOptionsAdded = true; + } + + std::pair minMax = _layerMinMaxCaps.at(_fitsLayerName); + std::unique_ptr t = + loadTextureFromFits(path, _fitsLayerName, minMax); + + if (!t) { + return; + } + + using FilterMode = ghoul::opengl::Texture::FilterMode; + if (_textureFilterProperty == static_cast(FilterMode::Nearest)) { + t->setFilter(FilterMode::Nearest); + } + else if (_textureFilterProperty == static_cast(FilterMode::Linear)) { + t->setFilter(FilterMode::Linear); + } + + glm::vec2 minMaxDataValues = minMaxTextureDataValues(t); + + File newFile = { + .status = File::FileStatus::Loaded, + .path = path, + .time = extractTriggerTimeFromFitsFileName(path), + .texture = std::move(t), + .dataMinMax = minMaxDataValues + }; + + const std::deque::const_iterator iter = std::upper_bound( + _files.begin(), + _files.end(), + newFile.time, + [](double timeRef, const File& fileRef) { + return timeRef < fileRef.time; + } + ); + std::deque::iterator it = _files.insert(iter, std::move(newFile)); + trackOldest(*it); +} + +glm::vec2 RenderableTimeVaryingFitsSphere::minMaxTextureDataValues( + std::unique_ptr& t) +{ + const glm::ivec3 dims = glm::ivec3(t->dimensions()); + const int width = dims.x; + const int height = dims.y; + + std::vector pixelValues; + pixelValues.reserve(width * height * 4); + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + glm::vec4 texel = t->texelAsFloat(x, y); + pixelValues.push_back(texel.r); + pixelValues.push_back(texel.g); + pixelValues.push_back(texel.b); + pixelValues.push_back(texel.a); + } + } + if (!pixelValues.empty()) { + float min = *std::min_element(pixelValues.begin(), pixelValues.end()); + float max = *std::max_element(pixelValues.begin(), pixelValues.end()); + return glm::vec2(min, max); + } + else { + return glm::vec2(0.f, 1.f); + } +} + +void RenderableTimeVaryingFitsSphere::extractMandatoryInfoFromSourceFolder() { + // Ensure that the source folder exists and then extract + // the files with the same extension as + namespace fs = std::filesystem; + const fs::path sourceFolder = _textureSource.stringValue(); + if (!std::filesystem::is_directory(sourceFolder)) { + throw ghoul::RuntimeError(std::format( + "Source folder '{}' for RenderableTimeVaryingFitsSphere is not a valid " + "directory", + sourceFolder + )); + } + // Extract all file paths from the provided folder + _files.clear(); + namespace fs = std::filesystem; + for (const fs::directory_entry& e : fs::directory_iterator(sourceFolder)) { + if (!e.is_regular_file()) { + continue; + } + if (e.path().extension() != ".fits") { + throw ghoul::RuntimeError(std::format( + "File extension for '{}' required to be .fits", e.path()) + ); + } + + readFileFromFits(e.path()); + } + // Ensure that there are available and valid source files left + if (_files.empty()) { + throw ghoul::RuntimeError( + "Source folder for RenderableTimeVaryingFitsSphere contains no files" + ); + } + + computeSequenceEndTime(); +} + +void RenderableTimeVaryingFitsSphere::update(const UpdateData& data) { + RenderableSphere::update(data); + + const double currentTime = data.time.j2000Seconds(); + const double deltaTime = global::timeManager->deltaTime(); + + if (!_dynamicFileDownloader && _loadingType == LoadingType::DynamicDownloading) { + const std::string& identifier = parent()->identifier(); + _dynamicFileDownloader = std::make_unique( + _dataID, + identifier, + _infoUrl, + _dataUrl, + _nFilesToQueue + ); + } + + if (_loadingType == LoadingType::DynamicDownloading) { + updateDynamicDownloading(currentTime, deltaTime); + } + + _inInterval = !_files.empty() && + currentTime >= _files[0].time && + currentTime < _sequenceEndTime; + + if (_inInterval) { + const size_t nextIdx = _activeTriggerTimeIndex + 1; + if ( + // true => We stepped back to a time represented by another state + currentTime < _files[_activeTriggerTimeIndex].time || + // true => We stepped forward to a time represented by another state + (nextIdx < _files.size() && currentTime >= _files[nextIdx].time)) + { + updateActiveTriggerTimeIndex(currentTime); + File& file = _files[_activeTriggerTimeIndex]; + if (file.status == File::FileStatus::Downloaded) { + std::pair minMax = _layerMinMaxCaps.at(_fitsLayerName); + file.texture = + loadTextureFromFits(file.path, _fitsLayerName, minMax); + using FilterMode = ghoul::opengl::Texture::FilterMode; + if (_textureFilterProperty == static_cast(FilterMode::Nearest)) { + file.texture->setFilter(FilterMode::Nearest); + } + else if (_textureFilterProperty == static_cast(FilterMode::Linear)) { + file.texture->setFilter(FilterMode::Linear); + } + file.status = File::FileStatus::Loaded; + trackOldest(file); + loadTexture(); + } + } + // The case when we jumped passed last file where nextIdx is not < file.size() + else if (currentTime >= _files[_activeTriggerTimeIndex].time && !_texture) { + loadTexture(); + } + } + + if (!_firstUpdate && _useColorMap && !_files.empty()) { + _dataMinMaxValues = _files[_activeTriggerTimeIndex].dataMinMax; + } + + if (_textureIsDirty) [[unlikely]] { + loadTexture(); + _textureIsDirty = false; + } +} + +void RenderableTimeVaryingFitsSphere::render(const RenderData& data, RendererTasks& task) +{ + if (_files.empty() || (!_inInterval && !_renderForever)) { + return; + } + RenderableSphere::render(data, task); +} + +void RenderableTimeVaryingFitsSphere::bindTexture() { + if (_texture) { + _texture->bind(); + } +} + +void RenderableTimeVaryingFitsSphere::updateActiveTriggerTimeIndex(double currentTime) { + auto iter = std::upper_bound( + _files.cbegin(), + _files.cend(), + currentTime, + [](double value, const File& f) { return value < f.time; } + ); + if (iter != _files.cend()) { + if (iter != _files.cbegin()) { + const ptrdiff_t idx = std::distance(_files.cbegin(), iter); + _activeTriggerTimeIndex = static_cast(idx - 1); + } + else { + _activeTriggerTimeIndex = 0; + } + } + else { + _activeTriggerTimeIndex = static_cast(_files.size()) - 1; + } +} + +void RenderableTimeVaryingFitsSphere::updateDynamicDownloading(double currentTime, + double deltaTime) +{ + _dynamicFileDownloader->update(currentTime, deltaTime); + const std::vector& filesToRead = + _dynamicFileDownloader->downloadedFiles(); + for (const std::filesystem::path& filePath : filesToRead) { + if (filePath.extension() == ".fits") { + readFileFromFits(filePath); + } + } + if (!filesToRead.empty()) { + computeSequenceEndTime(); + updateActiveTriggerTimeIndex(currentTime); + } + if (_firstUpdate) { + const bool isInInterval = !_files.empty() && + currentTime >= _files[0].time && + currentTime < _sequenceEndTime; + + if (isInInterval && + _activeTriggerTimeIndex != -1 && + _activeTriggerTimeIndex < _files.size()) + { + _firstUpdate = false; + loadTexture(); + } + } + // If all files are moved into _sourceFiles then we can + // empty the DynamicFileSequenceDownloader's downloaded files list + _dynamicFileDownloader->clearDownloaded(); +} + +void RenderableTimeVaryingFitsSphere::computeSequenceEndTime() { + if (_files.empty()) { + _sequenceEndTime = 0.f; + } + else if (_files.size() == 1) { + _sequenceEndTime = _files[0].time + 7200.f; + if (_loadingType == LoadingType::StaticLoading && !_renderForever) { + // TODO (2025-06-10, Elon) Alternativly check at construction and throw + // exeption. + LWARNING( + "Only one file in data set, but ShowAtAllTimes set to false. " + "Using arbitrary 2 hours to visualize data file instead" + ); + } + } + else if (_files.size() > 1) { + const double lastTriggerTime = _files[_files.size() - 1].time; + const double sequenceDuration = lastTriggerTime - _files[0].time; + const double averageCadence = + sequenceDuration / (static_cast(_files.size()) - 1.0); + // A multiplier of 3 to the average cadence is added at the end as a buffer + // 3 because if you start it just before new data came in, you might just be + // outside the sequence end time otherwise + _sequenceEndTime = lastTriggerTime + 3 * averageCadence; + } +} + +void RenderableTimeVaryingFitsSphere::loadTexture() { + if (_activeTriggerTimeIndex != -1 && + static_cast(_activeTriggerTimeIndex) < _files.size()) + { + _texture = _files[_activeTriggerTimeIndex].texture.get(); + showCorrectFileName(); + } +} + +void RenderableTimeVaryingFitsSphere::trackOldest(File& file) { + if (file.status == File::FileStatus::Loaded) { + std::deque::iterator it = + std::find(_loadedFiles.begin(), _loadedFiles.end(), &file); + if (it == _loadedFiles.end()) { + _loadedFiles.push_back(&file); + } + // Repopulate the queue if new File makes the queue full + if (!_loadedFiles.empty() && + _loadingType == LoadingType::DynamicDownloading && + _loadedFiles.size() >= _maxLoadedFiles) + { + File* oldest = _loadedFiles.front(); + // The edge case of when queue just got full and user jumped back to where + // they started which would make the oldes file in queue to be the active + // file. In that case we need to make sure we do not unload it + if (oldest == &file) { + return; + } + oldest->status = File::FileStatus::Downloaded; + oldest->texture = nullptr; + _loadedFiles.pop_front(); + } + } +} + +void RenderableTimeVaryingFitsSphere::showCorrectFileName() { + _textureSource = _files[_activeTriggerTimeIndex].path.filename().string(); +} + +} // namespace openspace diff --git a/modules/fitsfilereader/src/wsafitshelper.cpp b/modules/fitsfilereader/src/wsafitshelper.cpp new file mode 100644 index 0000000000..c7f334cd67 --- /dev/null +++ b/modules/fitsfilereader/src/wsafitshelper.cpp @@ -0,0 +1,156 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include +#include +#include +#include + +constexpr std::string_view _loggerCat = "RenderableTimeVaryingSphere"; + +using namespace CCfits; + +namespace openspace { + +std::unique_ptr loadTextureFromFits( + const std::filesystem::path& path, + size_t layerIndex, + const std::pair& minMax) +{ + try { + readFitsHeader(path); + std::unique_ptr file = std::make_unique(path.string(), Read, true); + if (!file.get()) { + LERROR(std::format( + "Failed to open, therefor removing file {}", path.string() + )); + std::filesystem::remove(path); + return nullptr; + } + // Convert fits path with fits-file-reader functions + const std::shared_ptr> fitsValues = + readImageInternal(file->pHDU()); + int layerSize = fitsValues->width * fitsValues->height; + + int nLayers = static_cast(fitsValues->contents.size()) / layerSize; + if (layerIndex > nLayers -1) { + LERROR( + "Chosen layer in fits file is not supported. Index too high. " + "First layer chosen instead" + ); + layerIndex = 0; + } + + std::valarray layerValues = + fitsValues->contents[std::slice(layerIndex*layerSize, layerSize, 1)]; + + float* imageData = new float[layerValues.size()]; + std::vector rgbLayers; + for (size_t i = 0; i < layerValues.size(); i++) { + // Normalization + float normalizedValue = + (layerValues[i] - minMax.first) / (minMax.second - minMax.first); + // Clamping causes overexposure above and below max and min values + // intentionally as desired by Nick Arge from WSA + normalizedValue = std::clamp(normalizedValue, 0.f, 1.f); + + imageData[i] = normalizedValue; + } + + // Create texture from imagedata + auto texture = std::make_unique( + imageData, + glm::size3_t(fitsValues->width, fitsValues->height, 1), + GL_TEXTURE_2D, + ghoul::opengl::Texture::Format::Red, + GL_RED, + GL_FLOAT + ); + // Tell it to use the single color channel as grayscale + convertTextureFormat(*texture, ghoul::opengl::Texture::Format::RGB); + texture->uploadTexture(); + return texture; + } + catch (const CCfits::FitsException& e) { + LERROR(std::format( + "Failed to open fits file '{}'. '{}'", path.string(), e.message() + )); + std::filesystem::remove(path); + return nullptr; + } +} + +void readFitsHeader(const std::filesystem::path& path) { + std::unique_ptr file = + std::make_unique(path.string(), CCfits::Read, true); + CCfits::PHDU& pHDU = file->pHDU(); + pHDU.readAllKeys(); + std::string val; + pHDU.readKey("CARRLONG", val); +} + +int nLayers(const std::filesystem::path& path) { + try { + std::unique_ptr file = std::make_unique(path.string(), Read, true); + if (!file.get()) { + LERROR(std::format("Failed to open fits file '{}'", path)); + return -1; + } + // Convert fits path with fits-file-reader functions + const std::shared_ptr> fitsValues = + readImageInternal(file->pHDU()); + int layerSize = fitsValues->width * fitsValues->height; + + return static_cast(fitsValues->contents.size() / layerSize); + } + catch (const CCfits::FitsException& e) { + LERROR(std::format( + "Failed to open fits file '{}'. '{}'", path, e.message() + )); + return 0; + } +} + +template +std::shared_ptr> readImageInternal(U& image) { + try { + std::valarray contents; + image.read(contents); + ImageData im = { + .contents = std::move(contents), + .width = static_cast(image.axis(0)), + .height = static_cast(image.axis(1)) + }; + return std::make_shared>(im); + } + catch (const CCfits::FitsException& e) { + LERROR(std::format( + "Could not read FITS layer. Error: {}", + e.message() + )); + } + return nullptr; +} + +} // namespace openspace diff --git a/modules/gaia/rendering/octreemanager.h b/modules/gaia/rendering/octreemanager.h index 2b9f9f8b4a..bca9d93561 100644 --- a/modules/gaia/rendering/octreemanager.h +++ b/modules/gaia/rendering/octreemanager.h @@ -260,7 +260,7 @@ private: * long as \p recursive is not set to false. * * \param node the node that should be removed - * \param deltaStars keeps track of how many stars that were removed. + * \param deltaStars keeps track of how many stars that were removed * \param recursive defines if decentents should be removed as well */ std::map> removeNodeFromCache(OctreeNode& node, @@ -294,7 +294,7 @@ private: * * \param node the node that should be inserted * \param mode the render mode that should be used - * \param deltaStars keeps track of how many stars that were added. + * \param deltaStars keeps track of how many stars that were added * \return the data to be inserted */ std::vector constructInsertData(const OctreeNode& node, diff --git a/modules/kameleonvolume/kameleonvolumereader.cpp b/modules/kameleonvolume/kameleonvolumereader.cpp index a0c6605881..4e2b6d64f8 100644 --- a/modules/kameleonvolume/kameleonvolumereader.cpp +++ b/modules/kameleonvolume/kameleonvolumereader.cpp @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -80,7 +81,7 @@ KameleonVolumeReader::KameleonVolumeReader(std::filesystem::path path) if (!std::filesystem::is_regular_file(_path)) { throw ghoul::FileNotFoundError(_path); } - + _kameleon = kameleonHelper::createKameleonObject(_path.string()); const long status = _kameleon->open(_path.string()); if (status != ccmc::FileReader::OK) { throw ghoul::RuntimeError(std::format( diff --git a/modules/server/include/logging/notificationlog.h b/modules/server/include/logging/notificationlog.h index 5d4e187313..09a369fe47 100644 --- a/modules/server/include/logging/notificationlog.h +++ b/modules/server/include/logging/notificationlog.h @@ -60,7 +60,7 @@ public: * Method that logs a message with a given level and category to the console. * * \param level The log level with which the message shall be logged - * \param category The category of this message. + * \param category The category of this message * \param message The message body of the log message */ void log(ghoul::logging::LogLevel level, std::string_view category, diff --git a/modules/space/rendering/renderablefluxnodes.cpp b/modules/space/rendering/renderablefluxnodes.cpp index ac697cab3f..02b7452b56 100644 --- a/modules/space/rendering/renderablefluxnodes.cpp +++ b/modules/space/rendering/renderablefluxnodes.cpp @@ -391,6 +391,7 @@ RenderableFluxNodes::RenderableFluxNodes(const ghoul::Dictionary& dictionary) LINFO("Assuming default value 1, meaning Emin03"); _goesEnergyBins.setValue(1); } + setupProperties(); } void RenderableFluxNodes::initialize() { @@ -420,7 +421,6 @@ void RenderableFluxNodes::initializeGL() { // Needed for alpha transparency setRenderBin(Renderable::RenderBin::PreDeferredTransparent); - setupProperties(); } void RenderableFluxNodes::definePropertyCallbackFunctions() { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ddc9b49769..10fc821867 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -172,6 +172,7 @@ set(OPENSPACE_SOURCE util/collisionhelper.cpp util/coordinateconversion.cpp util/distanceconversion.cpp + util/dynamicfilesequencedownloader.cpp util/ellipsoid.cpp util/factorymanager.cpp util/geodetic.cpp @@ -367,6 +368,7 @@ set(OPENSPACE_HEADER ${PROJECT_SOURCE_DIR}/include/openspace/util/coordinateconversion.h ${PROJECT_SOURCE_DIR}/include/openspace/util/distanceconstants.h ${PROJECT_SOURCE_DIR}/include/openspace/util/distanceconversion.h + ${PROJECT_SOURCE_DIR}/include/openspace/util/dynamicfilesequencedownloader.h ${PROJECT_SOURCE_DIR}/include/openspace/util/ellipsoid.h ${PROJECT_SOURCE_DIR}/include/openspace/util/factorymanager.h ${PROJECT_SOURCE_DIR}/include/openspace/util/factorymanager.inl diff --git a/src/navigation/navigationhandler_lua.inl b/src/navigation/navigationhandler_lua.inl index aea138766a..93b49e7bc8 100644 --- a/src/navigation/navigationhandler_lua.inl +++ b/src/navigation/navigationhandler_lua.inl @@ -1083,8 +1083,8 @@ localPositionFromGeo(std::string nodeIdentifier, double latitude, double longitu /** * Fly linearly to a specific distance in relation to the focus node. * - * \param distance The distance to fly to, in meters above the bounding sphere. - * \param duration An optional duration for the motion to take, in seconds. + * \param distance The distance to fly to, in meters above the bounding sphere + * \param duration An optional duration for the motion to take, in seconds */ [[codegen::luawrap]] void zoomToDistance(double distance, std::optional duration) { @@ -1126,8 +1126,8 @@ localPositionFromGeo(std::string nodeIdentifier, double latitude, double longitu * \param distance The distance to fly to, given as a multiple of the bounding sphere of * the current focus node bounding sphere. A value of 1 will result in a * position at a distance of one times the size of the bounding - * sphere away from the object. - * \param duration An optional duration for the motion, in seconds. + * sphere away from the object + * \param duration An optional duration for the motion, in seconds */ [[codegen::luawrap]] void zoomToDistanceRelative(double distance, std::optional duration) @@ -1169,11 +1169,11 @@ localPositionFromGeo(std::string nodeIdentifier, double latitude, double longitu * Fade rendering to black, jump to the specified node, and then fade in. This is done by * triggering another script that handles the logic. * - * \param navigationState A [NavigationState](#core_navigation_state) to jump to. + * \param navigationState A [NavigationState](#core_navigation_state) to jump to * \param useTimeStamp if true, and the provided NavigationState includes a timestamp, - * the time will be set as well. + * the time will be set as well * \param fadeDuration An optional duration for the fading. If not included, the - * property in Navigation Handler will be used. + * property in Navigation Handler will be used */ [[codegen::luawrap]] void jumpToNavigationState(ghoul::Dictionary navigationState, std::optional useTimeStamp, diff --git a/src/util/dynamicfilesequencedownloader.cpp b/src/util/dynamicfilesequencedownloader.cpp new file mode 100644 index 0000000000..cd0506a96b --- /dev/null +++ b/src/util/dynamicfilesequencedownloader.cpp @@ -0,0 +1,649 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2025 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "DynamicFileSequenceDownloader"; + + void trackFinishedDownloads(const std::filesystem::path& syncFilePath, + const std::filesystem::path& newFilePath) + { + std::unordered_set existingEntries; + std::ifstream inFile = std::ifstream(syncFilePath); + std::string line; + + // load existing entries + while (ghoul::getline(inFile, line)) { + if (!line.empty()) { + existingEntries.insert(std::filesystem::path(line).filename().string()); + } + } + inFile.close(); + + const std::string fileName = newFilePath.filename().string(); + const std::string filePath = newFilePath.string(); + + if (!existingEntries.contains(fileName)) { + std::ofstream outFile = std::ofstream(syncFilePath, std::ios::app); + if (outFile.is_open()) { + outFile << filePath << std::endl; + } + } + } + + std::string buildDataHttpRequest(double minTime, double maxTime, int dataID, + const std::string& baseUrl) + { + // formulate a min and a max time from time + // The thing is time might be "now" and no items + // ISO8601 format: yyyy-mm-ddThh:mm:ssZ + + // hour in seconds : 3600 + // days in seconds : 86400 + // 30 days in seconds : 2592000 + // 1 year in seconds : 31556926 + std::string_view min = openspace::Time(minTime).ISO8601(); + std::string_view max = openspace::Time(maxTime).ISO8601(); + + return std::format("{}{}&time.min={}&time.max={}", baseUrl, dataID, min, max); + } +} // namepace + +namespace openspace { + +DynamicFileSequenceDownloader::DynamicFileSequenceDownloader(int dataID, + const std::string& identifier, + std::string infoUrl, + std::string dataUrl, + size_t nOfFilesToQueue) + : _dataID(dataID) + , _infoUrl(std::move(infoUrl)) + , _dataUrl(std::move(dataUrl)) + , _nFilesToQueue(nOfFilesToQueue) +{ + _syncDir = absPath( + std::format("${{SYNC}}/dynamically_downloaded/{}/{}", dataID, identifier) + ); + + std::filesystem::path syncFile = _syncDir / std::format("{}.synced", dataID); + _trackSynced = syncFile; + // Just to create the folder + std::filesystem::path folder = _trackSynced.parent_path(); + std::filesystem::create_directories(folder); + // Just to create the file + { + std::ofstream file(_trackSynced, std::ios::app); + } + // Delete the files in the folder whos file name is not in the _trackSynced file + std::unordered_set keepFiles; + std::ifstream listFile = std::ifstream(_trackSynced); + std::string filename; + while (ghoul::getline(listFile, filename)) { + if (!filename.empty()) { + keepFiles.insert(std::filesystem::path(filename).filename().string()); + } + } + namespace fs = std::filesystem; + for (const fs::directory_entry& entry : fs::directory_iterator(folder)) { + if (!entry.is_regular_file()) { + continue; + } + std::string name = entry.path().filename().string(); + if (name != _trackSynced.filename().string() && + !keepFiles.contains(name)) + { + std::filesystem::remove(entry.path()); + } + } + + std::string httpInfoRequest = _infoUrl + std::to_string(_dataID); + requestDataInfo(httpInfoRequest); + std::string httpDataRequest = buildDataHttpRequest( + _dataMinTime, + _dataMaxTime, + _dataID, + _dataUrl + ); + requestAvailableFiles(httpDataRequest, _syncDir); +} + +void DynamicFileSequenceDownloader::deinitialize(bool cacheFiles) { + const std::vector& currentlyDownloadingFiles = filesCurrentlyDownloading(); + for (File* file : currentlyDownloadingFiles) { + file->download->cancel(); + file->download->wait(); + std::error_code ec; + std::filesystem::remove(file->path, ec); + if (ec) { + LERROR(std::format( + "Failed to delete unfinished file '{}'. {}", file->path, ec.message() + )); + } + else { + LINFO(std::format("Removing unfinished download:: {}", file->path)); + } + } + if (!cacheFiles) { + if (!std::filesystem::exists(_syncDir)) { + return; + } + namespace fs = std::filesystem; + for (const fs::directory_entry& file : fs::directory_iterator(_syncDir)) { + std::error_code ec; + std::filesystem::remove(file.path(), ec); + if (ec) { + LERROR(std::format( + "Failed to delete file '{}'. {}", file.path(), ec.message() + )); + } + } + } + if (std::filesystem::is_empty(_syncDir)) { + std::filesystem::remove(_syncDir); + } +} + +void DynamicFileSequenceDownloader::requestDataInfo(std::string httpInfoRequest) { + HttpMemoryDownload response = HttpMemoryDownload(httpInfoRequest); + response.start(); + response.wait(); + + bool success = false; + int attempt = 0; + constexpr int MaxRetries = 1; + + /******************** + * Example response + ********************* + * { + * "availability": { + * "startDate": "2017-07-01T00:42:02.0Z", + * "stopDate" : "2017-09-30T22:43:18.0Z" + * }, + * "description" : "WSA 4.4 field line trace from the SCS outer boundary to the + * source surface", + * "id" : 1177 + * } + */ + while (attempt <= MaxRetries && !success) { + try { + std::vector responseText = response.downloadedData(); + if (responseText.empty()) { + throw ghoul::RuntimeError("Empty HTTP response"); + } + nlohmann::json jsonResult = nlohmann::json::parse(responseText); + success = true; + _dataMinTime = Time::convertTime( + jsonResult["availability"]["startDate"].get() + ); + _dataMaxTime = Time::convertTime( + jsonResult["availability"]["stopDate"].get() + ); + } + catch (const nlohmann::json::parse_error& e) { + LWARNING(std::format("JSON parse error: {}", e.what())); + } + + if (!success) { + if (attempt < MaxRetries) { + LINFO(std::format("Retry number {}.", attempt + 1)); + std::this_thread::sleep_for(std::chrono::seconds(2)); + response.start(); + response.wait(); + } + else { + LERROR(std::format( + "Failed according to warning above with HTTP request of URL: {}", + httpInfoRequest + )); + } + } + attempt++; + } +} + +void DynamicFileSequenceDownloader::requestAvailableFiles(std::string httpDataRequest, + std::filesystem::path syncDir) +{ + // If it expands to more of a API call rather than a http-request, that code goes here + HttpMemoryDownload response = HttpMemoryDownload(httpDataRequest); + response.start(); + response.wait(); + + bool success = false; + int attempt = 0; + constexpr int MaxRetries = 1; + nlohmann::json jsonResult; + + /******************** + * Example response + ********************* + * { + * "dataID": 1234, + * "files": [ + * { + * "timestamp": "2017-07-01 00:42:02.0", + * "url": "https://iswa...fieldlines/trace_scs_outtoin/timestamp.osfls" + * }, + * { + * "timestamp": "2017-07-01 00:51:36.0", + * "url": "https://iswa...fieldlines/trace_scs_outtoin/timestamp.osfls" + * } + * ], + * "time.max": "2017-10-01 00:00:00.0", + * "time.min": "2017-06-01 00:00:00.0" + * } + * + * Note that requested time can be month 10 but last entry in list is month 07, + * meaning there are no more available files between month 7-10. + * *****************/ + while (attempt <= MaxRetries && !success) { + try { + std::vector data = response.downloadedData(); + if (data.empty()) { + throw ghoul::RuntimeError("Empty HTTP response"); + } + // @TODO (2025-06-10, Elon) What value is actually too large to handle? + if (data.size() > std::numeric_limits::max()) { + throw ghoul::RuntimeError( + "Http response with list of available files is too large, i.e. too " + "many files" + ); + } + + jsonResult = nlohmann::json::parse(data); + success = true; + } + catch (const nlohmann::json::parse_error& ex) { + LERROR(std::format("JSON parsing error: '{}'", ex.what())); + } + + if (!success) { + if (attempt < MaxRetries) { + LINFO(std::format("Retry nr {}.", attempt + 1)); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + response.start(); + response.wait(); + } + else { + LERROR(std::format( + "Failed according to warning above with HTTP request of URL: {}", + httpDataRequest + )); + } + } + attempt++; + } + + if (!success) { + return; + } + + int index = 0; + for (const nlohmann::json& element : jsonResult["files"]) { + std::string timestamp = element["timestamp"].get(); + std::string url = element["url"].get(); + + // An example of how one element in the list from the JSON-result look like: + // timestamp = "2022-11-13T16:14:00.000"; + // url = + // https://iswaA-webservice1.ccmc.gsfc.nasa.gov/ + // ...iswa_data_tree/model/solar/WSA5.X/fieldlines/ + // ...GONG_Z/trace_pfss_intoout/2022/11/2022-11-13T16-14-00.000.osfls"; + + std::string fileName = url.substr(url.find_last_of("//")); + std::filesystem::path destination = _syncDir; + destination += fileName; + + double time = Time::convertTime(timestamp); + File fileElement = { + .timestep = timestamp, + .time = time, + .URL = url, + .path = destination, + .cadence = 0, + .availableIndex = index + }; + if (std::filesystem::exists(destination)) { + fileElement.download = nullptr; + fileElement.state = File::State::Downloaded; + _downloadedFiles.push_back(destination); + trackFinishedDownloads(_trackSynced, destination); + } + else { + fileElement.download = std::make_unique(url, destination); + fileElement.state = File::State::Available; + } + _availableData.push_back(std::move(fileElement)); + ++index; + } + + const double cadence = calculateCadence(); + for (File& element : _availableData) { + element.cadence = cadence; + } +} + +double DynamicFileSequenceDownloader::calculateCadence() const { + double averageTime = 0.0; + + if (_availableData.size() < 2) { + // If 0 or 1 files there is no cadence + return averageTime; + } + const double time1 = Time::convertTime(_availableData.begin()->timestep); + const double timeN = Time::convertTime(_availableData.rbegin()->timestep); + averageTime = (timeN - time1) / _availableData.size(); + + return averageTime; +} + +void DynamicFileSequenceDownloader::downloadFile() { + ZoneScoped; + + // The MaxDownloads number is a educated guess. Ideally this would vary depending on + // each users computer and internet specifications. It allows at least a few files to + // download in parallel, but not too many to take up more bandwidth than it would + // allow the current files to download as fast as possible + constexpr int MaxDownloads = 4; + + if (_filesCurrentlyDownloading.size() < MaxDownloads && + !_queuedFilesToDownload.empty()) + { + File* dl = _queuedFilesToDownload.front(); + if (dl->state != File::State::OnQueue) { + throw ghoul::RuntimeError( + "Trying to download file from list of queued files, but its status is " + "not OnQueue" + ); + } + if (dl->download) { + dl->download->start(); + _filesCurrentlyDownloading.push_back(dl); + dl->state = File::State::Downloading; + _queuedFilesToDownload.erase(_queuedFilesToDownload.begin()); + } + } +} + +void DynamicFileSequenceDownloader::checkForFinishedDownloads() { + ZoneScoped; + + std::vector::iterator currentIt = _filesCurrentlyDownloading.begin(); + + // Since size of filesCurrentlyDownloading can change per iteration, keep size-call + for (size_t i = 0; i != _filesCurrentlyDownloading.size(); ++i) { + File* file = *currentIt; + HttpFileDownload* dl = file->download.get(); + + if (dl->hasSucceeded()) { + std::ifstream tempFile = std::ifstream(file->URL); + std::streampos size = tempFile.tellg(); + if (size == 0) { + LERROR(std::format("File '{}' is empty, removing", dl->destination())); + currentIt = _filesCurrentlyDownloading.erase(currentIt); + --i; + } + else { + _downloadedFiles.push_back(dl->destination()); + file->state = File::State::Downloaded; + trackFinishedDownloads(_trackSynced, file->path); + currentIt = _filesCurrentlyDownloading.erase(currentIt); + // if one is removed, i is reduced, else we'd skip one in the list + --i; + } + } + else if (dl->hasFailed()) { + LERROR(std::format("File '{}' failed to download. Removing file", file->URL)); + std::string filename; + size_t lastSlash = file->URL.find_last_of('/'); + if (lastSlash != std::string::npos && lastSlash + 1 < file->URL.size()) { + filename = file->URL.substr(lastSlash + 1); + } + + std::filesystem::path filepath = std::filesystem::path(_syncDir) / filename; + if (std::filesystem::exists(filepath)) { + std::error_code ec; + std::filesystem::remove(filepath, ec); + if (ec) { + LERROR(std::format( + "Failed to delete file '{}'. {}", filepath, ec.message() + )); + } + else { + LINFO(std::format( + "Deleted file after failed download: {}", filepath + )); + } + } + + currentIt = _filesCurrentlyDownloading.erase(currentIt); + // If one is removed, i is reduced, else we'd skip one in the list + --i; + } + // The file is not finished downloading, move on to next + else { + ++currentIt; + } + + // Since in the if statement one is removed and else statement it got incremented, + // check if at end + if (currentIt == _filesCurrentlyDownloading.end()) { + return; + } + } +} + +std::vector::iterator DynamicFileSequenceDownloader::closestFileToNow(double time) { + ZoneScoped; + std::vector::iterator closestIt = _availableData.begin(); + + std::vector::iterator it = std::lower_bound( + _availableData.begin(), + _availableData.end(), + time, + [](const File& file, double t) { return file.time < t; } + ); + + if (it == _availableData.end()) { + return std::prev(_availableData.end()); + } + if (it == _availableData.begin()) { + return it; + } + + std::vector::iterator prev = std::prev(it); + return _isForwardDirection ? prev : it; +} + +void DynamicFileSequenceDownloader::putInQueue() { + ZoneScoped; + + std::vector::iterator end; + if (_isForwardDirection) { + end = _availableData.end(); + } + else { + end = _availableData.begin(); + // To catch first file (since begin points to a file but end() does not) + if (_currentFile == end && _currentFile->state == File::State::Available) { + _queuedFilesToDownload.push_back(&*_currentFile); + _currentFile->state = File::State::OnQueue; + return; + } + } + + // If forward iterate from now to end. else reverse from now to begin + size_t nFileLimit = 0; + for (std::vector::iterator it = _currentFile; + it != end; + _isForwardDirection ? ++it : --it) + { + if (it->state == File::State::Available) { + _queuedFilesToDownload.push_back(&*it); + it->state = File::State::OnQueue; + } + ++nFileLimit; + // Exit out early if enough files are queued / already downloaded + if (nFileLimit == _nFilesToQueue) { + break; + } + } +} + +void DynamicFileSequenceDownloader::update(double time, double deltaTime) { + ZoneScoped; + + // First frame cannot guarantee time and deltatime has been set yet + if (_isFirstFrame) { + _isFirstFrame = false; + return; + } + if (_isSecondFrame) { + if (!_availableData.empty()) { + _isSecondFrame = false; + _currentFile = closestFileToNow(time); + } + return; + } + // More than 2hrs a second would generally be unfeasable + // for a regular internet connection to operate at + constexpr int SpeedThreshold = 7200; // 2 hours in seconds + if (std::abs(deltaTime) > SpeedThreshold) { + // Too fast, do nothing + if (!_hasNotifiedTooFast) { + LWARNING( + "Setting time speed faster than 2h per second will pause the downloader." + ); + _hasNotifiedTooFast = true; + } + return; + } + + // if delta time direction got changed + if (_isForwardDirection && deltaTime < 0 || !_isForwardDirection && deltaTime > 0) { + _isForwardDirection = !_isForwardDirection; + // Remove from queue when time changed, to start downloading most relevant files + for (File* file : _queuedFilesToDownload) { + file->state = File::State::Available; + } + _queuedFilesToDownload.clear(); + } + + if (_isForwardDirection && _currentFile != _availableData.end()) { + // if files are there and time is between next file (+1) and +2 file + // (meaning the this file is active from now till next file) + // change this to be next + if (_currentFile + 1 != _availableData.end() && + _currentFile + 2 != _availableData.end() && + (_currentFile + 1)->time < time && + (_currentFile + 2)->time > time) + { + _currentFile++; + } + // if its beyond the +2 file, arguably that can mean delta time is to fast + // and files might be missed. But we also know we went past beyond the next so + // we no longer know where we are so we reinitialize + else if (_currentFile + 1 != _availableData.end() && + _currentFile + 2 != _availableData.end() && + (_currentFile + 2)->time < time) + { + _currentFile = closestFileToNow(time); + } + // We've jumped back without interpolating to a previous time step, + // past circa 2 files worth of time and without changing delta time + // >>>>>>>we jumped to here>>>>>>>>>now>>>>>>> + else if (_currentFile->time - 2 * _currentFile->cadence > time) { + _currentFile = closestFileToNow(time); + } + } + else if (!_isForwardDirection && _currentFile != _availableData.begin()) { + // If file is there and time is between prev and this file + // then change this to be prev. Same goes here as if time is moving forward + // we will use forward 'usage', meaning file is active from now till next + if (_currentFile - 1 != _availableData.begin() && + _currentFile->time < time && + (_currentFile - 1)->time > time) + { + _currentFile--; + } + // If we are beyond the prev file, again delta time might be to fast, but we + // no longer know where we are so we reinitialize + else if (_currentFile - 1 != _availableData.begin() && + (_currentFile - 1)->time > time) + { + _currentFile = closestFileToNow(time); + } + // We've jumped forward without interpolating to a future time step, + // past circa 2 files worth of time and without changing delta time + // <<<<<time - 2 * _currentFile->cadence < time) { + _currentFile = closestFileToNow(time); + } + } + else if (_currentFile->download == nullptr && !_availableData.empty()) { + _currentFile = closestFileToNow(time); + } + + if (!_filesCurrentlyDownloading.empty()) { + checkForFinishedDownloads(); + } + + putInQueue(); + downloadFile(); +} + +const std::filesystem::path& DynamicFileSequenceDownloader::destinationDirectory() const { + return _syncDir; +} + +void DynamicFileSequenceDownloader::clearDownloaded() { + _downloadedFiles.clear(); +} + +bool DynamicFileSequenceDownloader::areFilesCurrentlyDownloading() const { + return !_filesCurrentlyDownloading.empty(); +} + +const std::vector& DynamicFileSequenceDownloader::filesCurrentlyDownloading() const +{ + return _filesCurrentlyDownloading; +} + +const std::vector& +DynamicFileSequenceDownloader::downloadedFiles() const +{ + return _downloadedFiles; +} + +} // openspace namespace diff --git a/src/util/httprequest.cpp b/src/util/httprequest.cpp index 5189eaa00d..b5237521f2 100644 --- a/src/util/httprequest.cpp +++ b/src/util/httprequest.cpp @@ -231,7 +231,10 @@ void HttpDownload::cancel() { bool HttpDownload::wait() { std::mutex conditionMutex; std::unique_lock lock(conditionMutex); - _downloadFinishCondition.wait(lock, [this]() { return _isFinished; }); + _downloadFinishCondition.wait( + lock, + [this]() { return _isFinished || _shouldCancel; } + ); if (_downloadThread.joinable()) { _downloadThread.join(); }