///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2025 Equinor ASA // // ResInsight is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU General Public License at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RimEnsembleFileSet.h" #include "Ensemble/RiaEnsembleImportTools.h" #include "RiaEnsembleNameTools.h" #include "RiaFilePathTools.h" #include "RiaStdStringTools.h" #include "RiaTextStringTools.h" #include "RimEnsembleFileSetCollection.h" #include "RimProject.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" #include "cafPdmUiComboBoxEditor.h" #include "cafPdmUiLineEditor.h" #include "cafPdmUiTextEditor.h" CAF_PDM_SOURCE_INIT( RimEnsembleFileSet, "EnsembleFileSet" ); namespace internal { QString placeholderString() { return "*"; } bool isEmptyOrWildcard( const QString& pattern ) { return pattern.isEmpty() || pattern.contains( placeholderString() ); } } // namespace internal //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimEnsembleFileSet::RimEnsembleFileSet() : fileSetChanged( this ) , nameChanged( this ) , m_useKey1( false ) , m_useKey2( false ) { CAF_PDM_InitObject( "Ensemble", ":/SummaryEnsemble.svg", "", "" ); CAF_PDM_InitField( &m_pathPattern, "PathPattern", QString(), "Path Pattern", "", "", "" ); CAF_PDM_InitField( &m_realizationSubSet, "RealizationSubSet", QString(), "Realization Filter", "", "Specify realization numbers. Example: 1-5, 8, 11-20, !4 will include 1, 2, 3, 5, 8, 11-20" ); CAF_PDM_InitFieldNoDefault( &m_ensembleInfo, "EnsembleInfo", "Info" ); m_ensembleInfo.registerGetMethod( this, &RimEnsembleFileSet::ensembleInfo ); m_ensembleInfo.uiCapability()->setUiReadOnly( true ); m_ensembleInfo.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() ); CAF_PDM_InitFieldNoDefault( &m_groupingMode, "GroupingMode", "Grouping Mode" ); CAF_PDM_InitScriptableField( &m_autoName, "CreateAutoName", true, "Auto Name" ); QString defaultText = RiaDefines::key1VariableName() + "-" + RiaDefines::key2VariableName(); QString tooltipText = QString( "Variables in template is supported, and will be replaced to create name. Example '%1'" ).arg( defaultText ); CAF_PDM_InitField( &m_nameTemplateString, "NameTemplateString", defaultText, "Name Template", "", tooltipText ); nameField()->uiCapability()->setUiReadOnly( true ); nameField()->xmlCapability()->disableIO(); setDeletable( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QStringList RimEnsembleFileSet::createPaths( const QString& extension ) const { if ( m_pathPattern().isEmpty() ) return {}; // Append extension to the path pattern and return list of files matching the pattern QString pathPatternWithExtension = m_pathPattern() + extension; auto realizationFilter = m_realizationSubSet(); if ( internal::isEmptyOrWildcard( realizationFilter ) ) { if ( m_realizationNumbersReadFromFiles.isEmpty() ) { QStringList paths; if ( m_groupingMode() == RiaDefines::EnsembleGroupingMode::RESINSIGHT_OPMFLOW_STRUCTURE ) { paths = RiaEnsembleImportTools::createPathsBySearchingFileSystem_obsolete( pathPatternWithExtension, internal::placeholderString(), "run" ); } else { paths = RiaEnsembleImportTools::createPathsBySearchingFileSystem( m_pathPattern(), extension, internal::placeholderString() ); } const auto [pattern, range] = RiaEnsembleImportTools::findPathPattern( paths, internal::placeholderString() ); m_realizationNumbersReadFromFiles = range; } auto numbers = RiaStdStringTools::valuesFromRangeSelection( m_realizationNumbersReadFromFiles.toStdString() ); if ( !numbers.empty() ) { auto minNumber = *std::min_element( numbers.begin(), numbers.end() ); auto maxNumber = *std::max_element( numbers.begin(), numbers.end() ); // Use the min and max numbers to create a realization filter, as this will the wildcard pattern. This makes it easy // to see the realizations with missing or incomplete data realizationFilter = QString( "%1-%2" ).arg( minNumber ).arg( maxNumber ); } else { realizationFilter = m_realizationNumbersReadFromFiles; } } return RiaEnsembleImportTools::createPathsFromPattern( pathPatternWithExtension, realizationFilter, internal::placeholderString() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::findAndSetPathPatternAndRangeString( const QStringList& filePaths ) { QStringList normalizedPaths; for ( const auto& path : filePaths ) { normalizedPaths.append( RiaFilePathTools::toInternalSeparator( path ) ); } const auto& [pattern, rangeString] = RiaEnsembleImportTools::findPathPattern( normalizedPaths, internal::placeholderString() ); // find the pattern without extension by finding . and remove rest of string auto noExtension = pattern; auto dotIndex = noExtension.lastIndexOf( '.' ); if ( dotIndex != -1 ) { noExtension = noExtension.left( dotIndex ); } m_pathPattern = noExtension; m_realizationNumbersReadFromFiles = rangeString; // When all files in an ensemble are selected, we could get the same behaviour if set the range string to a wildcard. Avoid this now, as // the performance is worse using a wildcard. // https://github.com/OPM/ResInsight/issues/12660 // https://github.com/OPM/ResInsight/issues/12663 m_realizationSubSet = rangeString; sendFileSetChangedSignal(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimEnsembleFileSet::pathPattern() const { return m_pathPattern; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setNameTemplate( const QString& name ) { m_nameTemplateString = name; nameChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::updateName( const std::set& existingEnsembleNames ) { const auto [key1, key2] = nameKeys(); QString templateText; if ( m_autoName ) { templateText = nameTemplateText(); } else { templateText = m_nameTemplateString(); } std::map keyValues = { { RiaDefines::key1VariableName(), QString::fromStdString( key1 ) }, { RiaDefines::key2VariableName(), QString::fromStdString( key2 ) }, }; auto candidateName = RiaTextStringTools::replaceTemplateTextWithValues( templateText, keyValues ); if ( m_autoName ) { candidateName = candidateName.trimmed(); // When using auto name, remove leading and trailing commas that may occur if key1 or key2 is empty if ( candidateName.startsWith( "," ) ) { candidateName = candidateName.mid( 1 ); } if ( candidateName.endsWith( "," ) ) { candidateName = candidateName.left( candidateName.length() - 1 ); } auto generateUniqueName = []( const QString& baseName, const std::set& existingNames ) { // Avoid identical ensemble names by appending a number QString uniqueName = baseName; int counter = 1; while ( existingNames.find( uniqueName ) != existingNames.end() ) { uniqueName = QString( "%1_%2" ).arg( baseName ).arg( counter++ ); } return uniqueName; }; candidateName = generateUniqueName( candidateName, existingEnsembleNames ); } if ( name() == candidateName ) return; setName( candidateName ); nameChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setUsePathKey1( bool useKey1 ) { m_useKey1 = useKey1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setUsePathKey2( bool useKey2 ) { m_useKey2 = useKey2; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setAutoName( bool autoName ) { m_autoName = autoName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) { if ( field == &m_pathPattern ) { if ( auto lineEdAttr = dynamic_cast( attribute ) ) { lineEdAttr->placeholderText = "Enter path pattern..."; } } else if ( field == &m_realizationSubSet ) { if ( auto lineEdAttr = dynamic_cast( attribute ) ) { lineEdAttr->placeholderText = "E.g. 0,1,4-10,!6. Use '*' for all."; } } else if ( field == &m_ensembleInfo ) { if ( auto* myAttr = dynamic_cast( attribute ) ) { myAttr->heightHint = -1; } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { uiOrdering.add( &m_autoName ); if ( !m_autoName() ) { uiOrdering.add( &m_nameTemplateString ); } uiOrdering.add( nameField() ); uiOrdering.add( &m_pathPattern ); uiOrdering.add( &m_realizationSubSet ); uiOrdering.add( &m_ensembleInfo ); uiOrdering.skipRemainingFields( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { if ( changedField == &m_nameTemplateString || changedField == &m_autoName ) { RimProject::current()->ensembleFileSetCollection()->updateFileSetNames(); return; } sendFileSetChangedSignal(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { menuBuilder << "RicCreateEnsembleFromFileSetFeature"; menuBuilder << "RicCreateReservoirGridEnsembleFromFileSetFeature"; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setPathPattern( const QString& pathPattern ) { m_pathPattern = pathPattern; m_realizationNumbersReadFromFiles.clear(); sendFileSetChangedSignal(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setRangeString( const QString& rangeString ) { sendFileSetChangedSignal(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimEnsembleFileSet::ensembleInfo() const { QString text = "Realization numbers detected on file system : " + m_realizationNumbersReadFromFiles; return text; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::pair RimEnsembleFileSet::nameKeys() const { std::string key1 = "Undefined KEY1"; std::string key2 = "Undefined KEY2"; if ( m_groupingMode() == RiaDefines::EnsembleGroupingMode::FMU_FOLDER_STRUCTURE ) { auto pathPattern = m_pathPattern + ".SMSPEC"; auto paths = RiaEnsembleImportTools::createPathsFromPattern( pathPattern, "0", internal::placeholderString() ); if ( !paths.empty() ) { auto fileNames = RiaEnsembleNameTools::groupFilePathsFmu( { paths.front().toStdString() } ); if ( !fileNames.empty() ) { key1 = fileNames.begin()->first.first; key2 = fileNames.begin()->first.second; } } } else if ( m_groupingMode() == RiaDefines::EnsembleGroupingMode::EVEREST_FOLDER_STRUCTURE ) { auto pathPattern = m_pathPattern + ".SMSPEC"; auto paths = RiaEnsembleImportTools::createPathsFromPattern( pathPattern, "0-1", internal::placeholderString() ); if ( paths.size() > 1 ) { auto name1 = RiaFilePathTools::toInternalSeparator( paths[0] ).toStdString(); auto name2 = RiaFilePathTools::toInternalSeparator( paths[1] ).toStdString(); auto parts1 = RiaStdStringTools::splitString( name1, '/' ); auto parts2 = RiaStdStringTools::splitString( name2, '/' ); size_t commonParts = 0; for ( size_t i = 0; i < std::min( parts1.size(), parts2.size() ); i++ ) { if ( parts1[i] == parts2[i] ) { commonParts++; } else { break; } } if ( commonParts == 1 ) { key2 = parts1[commonParts - 1]; } else if ( commonParts > 1 ) { key1 = parts1[commonParts - 2]; key2 = parts1[commonParts - 1]; } } } else if ( m_groupingMode() == RiaDefines::EnsembleGroupingMode::RESINSIGHT_OPMFLOW_STRUCTURE ) { auto pathPattern = m_pathPattern + ".SMSPEC"; auto paths = RiaEnsembleImportTools::createPathsFromPattern( pathPattern, "0", internal::placeholderString() ); if ( !paths.empty() ) { auto fileNames = RiaEnsembleNameTools::groupFilePathsOpm( { paths.front().toStdString() } ); if ( !fileNames.empty() ) { key1 = fileNames.begin()->first.first; key2 = fileNames.begin()->first.second; } } } return { key1, key2 }; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimEnsembleFileSet::nameTemplateText() const { QString text; if ( m_useKey1 ) text += RiaDefines::key1VariableName(); if ( m_useKey2 ) { if ( !text.isEmpty() ) text += ", "; text += RiaDefines::key2VariableName(); } return text; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::setGroupingMode( RiaDefines::EnsembleGroupingMode groupingMode ) { m_groupingMode = groupingMode; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiaDefines::EnsembleGroupingMode RimEnsembleFileSet::groupingMode() const { return m_groupingMode(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::sendFileSetChangedSignal() const { fileSetChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleFileSet::reload() { m_realizationNumbersReadFromFiles.clear(); sendFileSetChangedSignal(); }