/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2022 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr std::string_view _loggerCat = "RenderableFieldlinesSequence"; 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" }; constexpr openspace::properties::Property::PropertyInfo ColorQuantityInfo = { "ColorQuantity", "Quantity to Color By", "Quantity used to color lines if the 'By Quantity' color method is selected" }; constexpr openspace::properties::Property::PropertyInfo ColorMinMaxInfo = { "ColorQuantityMinMax", "ColorTable Min Value", "Value to map to the lowest and highest end of the color table" }; constexpr openspace::properties::Property::PropertyInfo ColorTablePathInfo = { "ColorTablePath", "Path to Color Table", "Color Table/Transfer Function to use for 'By Quantity' coloring" }; constexpr openspace::properties::Property::PropertyInfo ColorUniformInfo = { "Color", "Uniform Line Color", "The uniform color of lines shown when 'Color Method' is set to 'Uniform'" }; constexpr openspace::properties::Property::PropertyInfo ColorUseABlendingInfo = { "ABlendingEnabled", "Additive Blending", "Activate/deactivate additive blending" }; constexpr openspace::properties::Property::PropertyInfo DomainEnabledInfo = { "DomainEnabled", "Domain Limits", "Enable/Disable domain limits" }; constexpr openspace::properties::Property::PropertyInfo DomainXInfo = { "LimitsX", "X-limits", "Valid range along the X-axis. [Min, Max]" }; constexpr openspace::properties::Property::PropertyInfo DomainYInfo = { "LimitsY", "Y-limits", "Valid range along the Y-axis. [Min, Max]" }; constexpr openspace::properties::Property::PropertyInfo DomainZInfo = { "LimitsZ", "Z-limits", "Valid range along the Z-axis. [Min, Max]" }; constexpr openspace::properties::Property::PropertyInfo DomainRInfo = { "LimitsR", "Radial limits", "Valid radial range. [Min, Max]" }; constexpr openspace::properties::Property::PropertyInfo FlowColorInfo = { "FlowColor", "Flow Color", "Color of particles flow direction indication" }; constexpr openspace::properties::Property::PropertyInfo FlowEnabledInfo = { "FlowEnabled", "Flow Direction", "Toggles the rendering of moving particles along the lines. Can, for example, " "illustrate magnetic flow" }; constexpr openspace::properties::Property::PropertyInfo FlowReversedInfo = { "Reversed", "Reversed Flow", "Toggle to make the flow move in the opposite direction" }; constexpr openspace::properties::Property::PropertyInfo FlowParticleSizeInfo = { "ParticleSize", "Particle Size", "Size of the particles" }; constexpr openspace::properties::Property::PropertyInfo FlowParticleSpacingInfo = { "ParticleSpacing", "Particle Spacing", "Spacing inbetween particles" }; constexpr openspace::properties::Property::PropertyInfo FlowSpeedInfo = { "Speed", "Speed", "Speed of the flow" }; constexpr openspace::properties::Property::PropertyInfo MaskingEnabledInfo = { "MaskingEnabled", "Masking", "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" }; constexpr openspace::properties::Property::PropertyInfo MaskingMinMaxInfo = { "MaskingMinLimit", "Lower Limit", "Lower and upper limit of the valid masking range" }; constexpr openspace::properties::Property::PropertyInfo MaskingQuantityInfo = { "MaskingQuantity", "Quantity used for Masking", "Quantity used for masking" }; constexpr openspace::properties::Property::PropertyInfo LineWidthInfo = { "LineWidth", "Line Width", "This value specifies the line width of the fieldlines" }; constexpr openspace::properties::Property::PropertyInfo TimeJumpButtonInfo = { "TimeJumpToStart", "Jump to Start Of Sequence", "Performs a time jump to the start of the sequence" }; struct [[codegen::Dictionary(RenderableFieldlinesSequence)]] Parameters { enum class SourceFileType { Cdf, Json, Osfls }; // Input file type. Should be cdf, json or osfls SourceFileType inputFileType; // Path to folder containing the input files std::filesystem::path sourceFolder [[codegen::directory()]]; // 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(ColorUniformInfo.description)]] std::optional color [[codegen::color()]]; // A list of paths to transferfunction .txt files containing color tables // 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 std::optional> colorTableRanges; // Enables flow, showing the direction, but not accurate speed, that particles // would be traveling std::optional flowEnabled; // [[codegen::verbatim(FlowColorInfo.description)]] std::optional flowColor [[codegen::color()]]; // [[codegen::verbatim(FlowReversedInfo.description)]] std::optional reversedFlow; // [[codegen::verbatim(FlowParticleSizeInfo.description)]] std::optional particleSize; // [[codegen::verbatim(FlowParticleSpacingInfo.description)]] std::optional particleSpacing; // [[codegen::verbatim(FlowSpeedInfo.description)]] std::optional flowSpeed; // [[codegen::verbatim(MaskingEnabledInfo.description)]] std::optional maskingEnabled; // [[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 std::optional> maskingRanges; // 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; // 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; }; #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"); } RenderableFieldlinesSequence::RenderableFieldlinesSequence( const ghoul::Dictionary& dictionary) : Renderable(dictionary) , _colorGroup({ "Color" }) , _colorMethod(ColorMethodInfo, properties::OptionProperty::DisplayType::Radio) , _colorQuantity(ColorQuantityInfo, properties::OptionProperty::DisplayType::Dropdown) , _colorQuantityMinMax( ColorMinMaxInfo, glm::vec2(-0.f, 100.f), glm::vec2(-5000.f), glm::vec2(5000.f) ) , _colorTablePath(ColorTablePathInfo) , _colorUniform( ColorUniformInfo, glm::vec4(0.3f, 0.57f, 0.75f, 0.5f), glm::vec4(0.f), glm::vec4(1.f) ) , _colorABlendEnabled(ColorUseABlendingInfo, true) , _domainEnabled(DomainEnabledInfo, true) , _domainGroup({ "Domain" }) , _domainX(DomainXInfo) , _domainY(DomainYInfo) , _domainZ(DomainZInfo) , _domainR(DomainRInfo) , _flowColor( FlowColorInfo, glm::vec4(0.96f, 0.88f, 0.8f, 0.5f), 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( MaskingMinMaxInfo, glm::vec2(0.f, 100.f), glm::vec2(-5000.f), glm::vec2(5000.f) ) , _maskingQuantity( MaskingQuantityInfo, properties::OptionProperty::DisplayType::Dropdown ) , _lineWidth(LineWidthInfo, 1.f, 1.f, 20.f) , _jumpToStartBtn(TimeJumpButtonInfo) { const Parameters p = codegen::bake(dictionary); // 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; } else { _tracingVariable = "b"; // Magnetic field variable as default LWARNING(fmt::format( "No tracing variable, using default '{}'", _tracingVariable )); } 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(fmt::format( "FieldlinesSequence {} is not a valid directory", sourcePath.string() )); } // 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); } } 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); std::transform( sub.begin(), sub.end(), sub.begin(), [](char c) { return static_cast(::tolower(c)); } ); return sub != fileTypeString; } ), _sourceFiles.end() ); // Ensure that there are available and valid source files left if (_sourceFiles.empty()) { LERROR(fmt::format( "{} contains no {} files", sourcePath.string(), fileTypeString )); } _extraVars = p.extraVariables.value_or(_extraVars); _flowEnabled = p.flowEnabled.value_or(_flowEnabled); _flowColor = p.flowColor.value_or(_flowColor); _flowReversed = p.reversedFlow.value_or(_flowReversed); _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); if (p.colorTablePaths.has_value()) { _colorTablePaths = p.colorTablePaths.value(); } else { // Set a default color table, just in case the (optional) user defined paths are // corrupt or not provided _colorTablePaths.push_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); } } else { _colorMethod = static_cast(ColorMethod::Uniform); } if (p.colorQuantity.has_value()) { _colorMethod = static_cast(ColorMethod::ByQuantity); _colorQuantityTemp = *p.colorQuantity; } if (p.colorTableRanges.has_value()) { _colorTableRanges = *p.colorTableRanges; } else { _colorTableRanges.push_back(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.maskingRanges.has_value()) { _maskingRanges = *p.maskingRanges; } else { _maskingRanges.push_back(glm::vec2(-100000.f, 100000.f)); // some default values } _outputFolderPath = p.outputFolder.value_or(_outputFolderPath); if (!_outputFolderPath.empty() && !std::filesystem::is_directory(_outputFolderPath)) { _outputFolderPath.clear(); LERROR(fmt::format( "The specified output path: '{}', does not exist", _outputFolderPath )); } _scalingFactor = p.scaleToMeters.value_or(_scalingFactor); } void RenderableFieldlinesSequence::initialize() { _transferFunction = std::make_unique( absPath(_colorTablePaths[0]).string() ); } 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); glGenBuffers(1, &_vertexMaskingBuffer); // Needed for additive blending setRenderBin(Renderable::RenderBin::Overlay); } // Returns fls::Model::Invalid if it fails to extract mandatory information fls::Model stringToModel(std::string str) { std::transform( str.begin(), str.end(), str.begin(), [](char c) { return static_cast(::tolower(c)); } ); 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(fmt::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); // Add Property Groups addPropertySubOwner(_colorGroup); addPropertySubOwner(_domainGroup); addPropertySubOwner(_flowGroup); if (hasExtras) { addPropertySubOwner(_maskingGroup); } // Add Properties to the groups _colorUniform.setViewOption(properties::Property::ViewOptions::Color); _colorGroup.addProperty(_colorUniform); _domainGroup.addProperty(_domainX); _domainGroup.addProperty(_domainY); _domainGroup.addProperty(_domainZ); _domainGroup.addProperty(_domainR); _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]; _colorTableRanges.resize(nExtraQuantities, _colorTableRanges.back()); _maskingRanges.resize(nExtraQuantities, _maskingRanges.back()); } 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]; }); _colorTablePath.onChange([this]() { _transferFunction->setPath(_colorTablePath); }); _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; } } void RenderableFieldlinesSequence::setModelDependentConstants() { const fls::Model simulationModel = _states[0].model(); float limit = 100.f; // Just used as a default value. switch (simulationModel) { case fls::Model::Batsrus: _scalingFactor = fls::ReToMeter; limit = 300.f; // Should include a long magnetotail break; case fls::Model::Enlil: _flowReversed = true; _scalingFactor = fls::AuToMeter; limit = 50.f; // Should include Plutos furthest distance from the Sun break; case fls::Model::Pfss: _scalingFactor = fls::RsToMeter; limit = 100.f; // Just a default value far away from the solar surface break; default: break; } _domainX.setMinValue(glm::vec2(-limit)); _domainX.setMaxValue(glm::vec2(limit)); _domainY.setMinValue(glm::vec2(-limit)); _domainY.setMaxValue(glm::vec2(limit)); _domainZ.setMinValue(glm::vec2(-limit)); _domainZ.setMaxValue(glm::vec2(limit)); // Radial should range from 0 out to a corner of the cartesian box: // sqrt(3) = 1.732..., 1.75 is a nice and round number _domainR.setMinValue(glm::vec2(0.f)); _domainR.setMaxValue(glm::vec2(limit * 1.75f)); _domainX = glm::vec2(-limit, limit); _domainY = glm::vec2(-limit, limit); _domainZ = glm::vec2(-limit, limit); _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(fmt::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(fmt::format("Could not open seed points file '{}'", seedFilePath)); outMap.clear(); return {}; } LDEBUG(fmt::format("Reading seed points from file '{}'", seedFilePath)); std::string line; std::vector outVec; while (std::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(fmt::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 (std::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; glDeleteBuffers(1, &_vertexPositionBuffer); _vertexPositionBuffer = 0; glDeleteBuffers(1, &_vertexColorBuffer); _vertexColorBuffer = 0; glDeleteBuffers(1, &_vertexMaskingBuffer); _vertexMaskingBuffer = 0; if (_shaderProgram) { global::renderEngine->removeRenderProgram(_shaderProgram.get()); _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; } std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } bool RenderableFieldlinesSequence::isReady() const { return _shaderProgram != nullptr; } void RenderableFieldlinesSequence::render(const RenderData& data, RendererTasks&) { if (_activeTriggerTimeIndex == -1) { return; } _shaderProgram->activate(); // Calculate Model View MatrixProjection const glm::dmat4 rotMat = glm::dmat4(data.modelTransform.rotation); const glm::dmat4 modelMat = glm::translate(glm::dmat4(1.0), data.modelTransform.translation) * rotMat * 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("colorMethod", _colorMethod); _shaderProgram->setUniform("lineColor", _colorUniform); _shaderProgram->setUniform("usingDomain", _domainEnabled); _shaderProgram->setUniform("usingMasking", _maskingEnabled); 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]); } if (_maskingEnabled) { _shaderProgram->setUniform("maskingRange", _maskingRanges[_maskingQuantity]); } _shaderProgram->setUniform("domainLimR", _domainR.value() * _scalingFactor); _shaderProgram->setUniform("domainLimX", _domainX.value() * _scalingFactor); _shaderProgram->setUniform("domainLimY", _domainY.value() * _scalingFactor); _shaderProgram->setUniform("domainLimZ", _domainZ.value() * _scalingFactor); // Flow/Particles _shaderProgram->setUniform("flowColor", _flowColor); _shaderProgram->setUniform("usingParticles", _flowEnabled); _shaderProgram->setUniform("particleSize", _flowParticleSize); _shaderProgram->setUniform("particleSpacing", _flowParticleSpacing); _shaderProgram->setUniform("particleSpeed", _flowSpeed); _shaderProgram->setUniform( "time", global::windowDelegate->applicationTime() * (_flowReversed ? -1 : 1) ); bool additiveBlending = false; if (_colorABlendEnabled) { additiveBlending = true; glDepthMask(false); glBlendFunc(GL_SRC_ALPHA, GL_ONE); } glBindVertexArray(_vertexArrayObject); #ifndef __APPLE__ glLineWidth(_lineWidth); #else glLineWidth(1.f); #endif glMultiDrawArrays( GL_LINE_STRIP, _states[_activeStateIndex].lineStart().data(), _states[_activeStateIndex].lineCount().data(), static_cast(_states[_activeStateIndex].lineStart().size()) ); glBindVertexArray(0); _shaderProgram->deactivate(); if (additiveBlending) { // Restores OpenGL Rendering State global::renderEngine->openglStateCache().resetBlendState(); global::renderEngine->openglStateCache().resetDepthState(); } } 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); glBindVertexArray(0); } void RenderableFieldlinesSequence::updateVertexPositionBuffer() { if (_activeStateIndex == -1) { return; } glBindVertexArray(_vertexArrayObject); glBindBuffer(GL_ARRAY_BUFFER, _vertexPositionBuffer); const std::vector& vertPos = _states[_activeStateIndex].vertexPositions(); glBufferData( GL_ARRAY_BUFFER, vertPos.size() * sizeof(glm::vec3), vertPos.data(), GL_STATIC_DRAW ); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); unbindGL(); } 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 ); if (isSuccessful) { glBufferData( GL_ARRAY_BUFFER, quantities.size() * sizeof(float), quantities.data(), GL_STATIC_DRAW ); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, 0); 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 ); if (isSuccessful) { glBufferData( GL_ARRAY_BUFFER, maskings.size() * sizeof(float), maskings.data(), GL_STATIC_DRAW ); glEnableVertexAttribArray(2); glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, 0); unbindGL(); } } } // namespace openspace