///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2026 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 "RimSumoDataSource.h" #include "RiaLogging.h" #include "RiaStdStringTools.h" #include "Cloud/RimSummaryEnsembleSumo.h" #include "Rim3dOverlayInfoConfig.h" #include "RimEclipseView.h" #include "RimEclipseViewCollection.h" #include "RimProject.h" #include "RimReservoirGridEnsembleSumo.h" #include "RimRoffCaseSumo.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmPointer.h" #include "cafPdmUiLineEditor.h" #include "cafPdmUiTextEditor.h" #include #include #include CAF_PDM_SOURCE_INIT( RimSumoDataSource, "RimSumoDataSource", "RimSummarySumoDataSource", "RimSumoGridDataSource" ); //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimSumoDataSource::RimSumoDataSource() { CAF_PDM_InitObject( "Sumo Data Source", ":/CloudBlobs.svg" ); CAF_PDM_InitFieldNoDefault( &m_caseId, "CaseId", "Case Id" ); CAF_PDM_InitFieldNoDefault( &m_assetName, "AssetName", "Asset Name" ); CAF_PDM_InitFieldNoDefault( &m_caseName, "CaseName", "Case Name" ); CAF_PDM_InitFieldNoDefault( &m_ensembleName, "EnsembleName", "Ensemble Name" ); CAF_PDM_InitFieldNoDefault( &m_customName, "CustomName", "Custom Name" ); CAF_PDM_InitFieldNoDefault( &m_availableRealizationIds, "RealizationIds", "Available Realization Ids" ); m_availableRealizationIds.uiCapability()->setUiHidden( true ); m_availableRealizationIds.xmlCapability()->disableIO(); CAF_PDM_InitField( &m_realizationFilter, "RealizationFilter", QString(), "Realization Filter", "", "Specify realization numbers. Example: 1-5, 8, 11-20, !4 will include 1, 2, 3, 5, 8, 11-20" ); m_realizationFilter.uiCapability()->setAttribute( caf::PdmUiLineEditor::Keys::PLACEHOLDER_TEXT, "E.g. 0,1,4-10,!6. Use '*' for all." ); CAF_PDM_InitFieldNoDefault( &m_realizationFilterInfo, "RealizationFilterInfo", "Info" ); m_realizationFilterInfo.uiCapability()->setAttribute( caf::PdmUiTextEditor::Keys::HEIGHT_HINT, -1 ); m_realizationFilterInfo.registerGetMethod( this, &RimSumoDataSource::realizationFilterInfoText ); m_realizationFilterInfo.uiCapability()->setUiReadOnly( true ); m_realizationFilterInfo.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() ); CAF_PDM_InitFieldNoDefault( &m_vectorNames, "VectorNames", "Vector Names" ); m_vectorNames.uiCapability()->setUiHidden( true ); m_vectorNames.xmlCapability()->disableIO(); CAF_PDM_InitFieldNoDefault( &m_gridNames, "GridNames", "Grid Names" ); m_gridNames.uiCapability()->setUiHidden( true ); m_gridNames.xmlCapability()->disableIO(); CAF_PDM_InitFieldNoDefault( &m_selectedGridName, "GridName", "Grid Name" ); CAF_PDM_InitField( &m_doComputeMobileVolumeWeightedMean, "DoComputeMobileVolumeWeightedMean", false, "Compute Mobile Volume Weighted Mean", "", "Show the mobile volume weighted mean in the 3D info box. This downloads the PORV and SWCR grid " "properties for every realization." ); setDeletable( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- SumoCaseId RimSumoDataSource::caseId() const { return SumoCaseId( m_caseId() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setCaseId( const SumoCaseId& caseId ) { m_caseId = caseId.get(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::assetName() const { return m_assetName(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setAssetName( const QString& assetName ) { m_assetName = assetName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::caseName() const { return m_caseName(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setCaseName( const QString& caseName ) { m_caseName = caseName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::ensembleName() const { return m_ensembleName(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setEnsembleName( const QString& ensembleName ) { m_ensembleName = ensembleName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSumoDataSource::availableRealizationIds() const { return m_availableRealizationIds(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RimSumoDataSource::hasFetchedRealizations() const { return m_hasFetchedRealizations; } //-------------------------------------------------------------------------------------------------- /// The available realizations are the source of truth. An empty filter selects all of them. //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setAvailableRealizationIds( const std::vector& realizationIds ) { m_availableRealizationIds = realizationIds; m_hasFetchedRealizations = true; // Show the full range instead of an empty field. Only an empty filter is replaced, keeping a user // or project defined filter. Selection is unchanged, as empty and full range both select all. if ( m_realizationFilter().trimmed().isEmpty() ) { m_realizationFilter = availableRealizationsRangeText(); } } //-------------------------------------------------------------------------------------------------- /// The subset of available realizations matching the realization filter. An empty filter (or '*') /// selects all available realizations. //-------------------------------------------------------------------------------------------------- std::vector RimSumoDataSource::selectedRealizationIds() const { const auto& available = m_availableRealizationIds(); auto filter = m_realizationFilter(); if ( filter.trimmed().isEmpty() || filter.contains( '*' ) ) { return available; } auto selectedValues = RiaStdStringTools::valuesFromRangeSelection( filter.toStdString() ); std::vector result; for ( const auto& realizationId : available ) { bool ok = false; int value = realizationId.toInt( &ok ); if ( ok && selectedValues.count( value ) > 0 ) { result.push_back( realizationId ); } } return result; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSumoDataSource::vectorNames() const { return m_vectorNames(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setVectorNames( const std::vector& vectorNames ) { m_vectorNames = vectorNames; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSumoDataSource::gridNames() const { return m_gridNames(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setGridNames( const std::vector& gridNames ) { m_gridNames = gridNames; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::selectedGridName() const { return m_selectedGridName(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::setSelectedGridName( const QString& gridName ) { m_selectedGridName = gridName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RimSumoDataSource::doComputeMobileVolumeWeightedMean() const { return m_doComputeMobileVolumeWeightedMean(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::updateName() { if ( !m_customName().isEmpty() ) { setName( m_customName() ); return; } auto name = QString( "%1 (%2)" ).arg( ensembleName(), caseName() ); setName( name ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { menuBuilder.addCmdFeature( "RicCreateSumoEnsembleFeature" ); menuBuilder.addCmdFeature( "RicCreateSumoReservoirGridEnsembleFeature" ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { auto group = uiOrdering.addNewGroup( "General" ); group->add( nameField() ); nameField()->uiCapability()->setUiReadOnly( true ); group->add( &m_caseId ); m_caseId.uiCapability()->setUiReadOnly( true ); group->add( &m_assetName ); m_assetName.uiCapability()->setUiReadOnly( true ); group->add( &m_caseName ); m_caseName.uiCapability()->setUiReadOnly( true ); group->add( &m_ensembleName ); m_ensembleName.uiCapability()->setUiReadOnly( true ); group->add( &m_customName ); auto ensembleGroup = uiOrdering.addNewGroup( "Ensemble Selection" ); ensembleGroup->add( &m_realizationFilter ); ensembleGroup->add( &m_realizationFilterInfo ); auto gridGroup = uiOrdering.addNewGroup( "Grid Selection" ); gridGroup->add( &m_selectedGridName ); gridGroup->add( &m_doComputeMobileVolumeWeightedMean ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QList RimSumoDataSource::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) { QList options; if ( fieldNeedingOptions == &m_selectedGridName ) { for ( const auto& gridName : m_gridNames() ) { options.push_back( caf::PdmOptionItemInfo( gridName, gridName ) ); } } return options; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { if ( changedField == &m_customName ) { updateName(); } else if ( changedField == &m_realizationFilter ) { onRealizationFilterChanged(); } else if ( changedField == &m_doComputeMobileVolumeWeightedMean ) { updateGridCaseEnsembles(); } } //-------------------------------------------------------------------------------------------------- /// Propagate a change in the realization filter to the ensembles created from this data source, so /// editing the filter updates the realization cases (and the connected plots/views). //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::onRealizationFilterChanged() { // Update any summary ensembles created from this data source (same behaviour as summary ensembles // loaded from disk, see RimSummaryFileSetEnsemble::onFileSetChanged). for ( auto ensemble : objectsWithReferringPtrFieldsOfType() ) { ensemble->onRealizationSelectionChanged(); } // Update any grid case ensembles created from this data source. updateGridCaseEnsembles(); } //-------------------------------------------------------------------------------------------------- /// Synchronize the realization grid cases (RimRoffCaseSumo) of every grid ensemble created from this /// data source with the current realization filter: remove cases for deselected realizations and add /// cases for newly selected ones. The grid name of each ensemble is preserved. //-------------------------------------------------------------------------------------------------- void RimSumoDataSource::updateGridCaseEnsembles() { // Synchronizing against realizations that have not been fetched would read "not known yet" as "nothing // selected" and remove the realization cases of every grid ensemble. That is what a project load followed // by an edit of the realization filter would otherwise do. const bool realizationsAreKnown = hasFetchedRealizations(); std::set selectedRealizations; for ( const auto& realizationId : selectedRealizationIds() ) { bool ok = false; int value = realizationId.toInt( &ok ); if ( ok ) selectedRealizations.insert( value ); } // The ensembles hold the back link to this data source, so they are found even when the realization // filter has left them without any cases. for ( auto ensemble : objectsWithReferringPtrFieldsOfType() ) { std::vector gridCases; for ( auto eclipseCase : ensemble->sourceCases() ) { if ( auto gridCase = dynamic_cast( eclipseCase ) ) gridCases.push_back( gridCase ); } // All cases in an ensemble share the same grid, and the ensemble knows which one, so the name is // available also when the filter has left it without any cases. const QString gridName = ensemble->sumoGridName(); if ( gridName.isEmpty() ) continue; if ( ensemble->doComputeMobileVolumeWeightedMean() != m_doComputeMobileVolumeWeightedMean() ) { ensemble->setDoComputeMobileVolumeWeightedMean( m_doComputeMobileVolumeWeightedMean() ); // The 3D info box reads the setting when it is updated, so refresh it to apply the change immediately. if ( auto viewColl = ensemble->viewCollection() ) { for ( auto view : viewColl->views() ) { if ( view->overlayInfoConfig() ) view->overlayInfoConfig()->update3DInfo(); } } } // Everything above applies whether or not the realizations are known. Adding and removing cases below // does not, so leave the ensemble as it is until the realizations have been fetched. if ( !realizationsAreKnown ) continue; // A realization the grid does not exist for would only produce a case failing to load. An empty list // means the realizations of the grid are not known, then do not filter on them. std::set realizationsToSelect = selectedRealizations; if ( const auto gridRealizations = ensemble->gridRealizations(); !gridRealizations.empty() ) { std::erase_if( realizationsToSelect, [&gridRealizations]( int realization ) { return std::ranges::find( gridRealizations, realization ) == gridRealizations.end(); } ); } // The selection can be changed after the ensemble was created, so report the realizations left out // here as well, not only when the ensemble is created. The ensemble reports the current set in its // property panel and marks itself in the tree, see RimReservoirGridEnsembleSumo. if ( const auto missing = ensemble->realizationsWithoutGridData(); !missing.empty() ) { RiaLogging::warning( QString( "Grid '%1' has no data for %2 of the selected realizations, no cases created " "for them: %3" ) .arg( gridName ) .arg( missing.size() ) .arg( QString::fromStdString( RiaStdStringTools::formatRangeSelection( missing ) ) ) .toStdString() ); } std::set currentRealizations; for ( auto gridCase : gridCases ) { currentRealizations.insert( gridCase->realization() ); } // Add cases for newly selected realizations first, so a view displaying a removed realization // can be repointed to one of these instead of being deleted. for ( int realization : realizationsToSelect ) { if ( currentRealizations.find( realization ) != currentRealizations.end() ) continue; if ( auto* gridCase = RimRoffCaseSumo::createFromDataSource( this, gridName, realization ) ) { ensemble->addCase( gridCase ); if ( auto project = RimProject::current() ) project->assignCaseIdToCase( gridCase ); } } // Remove cases for realizations no longer selected. Remember any views that displayed them, so // they can be repointed to a surviving case below (a deleted case auto-nulls the view's case, // which would otherwise make the view disappear, e.g. when changing 30-40 to 31-40). std::vector orphanedViews; for ( auto gridCase : gridCases ) { if ( realizationsToSelect.find( gridCase->realization() ) != realizationsToSelect.end() ) continue; if ( auto viewColl = ensemble->viewCollection() ) { for ( auto view : viewColl->views() ) { if ( view->eclipseCase() == gridCase ) orphanedViews.push_back( view ); } } ensemble->removeCase( gridCase ); delete gridCase; } // Repoint orphaned views to a surviving case, or delete them if no realizations remain. const auto remainingCases = ensemble->sourceCases(); for ( auto view : orphanedViews ) { if ( !remainingCases.empty() ) { view->setEclipseCase( remainingCases.front() ); view->loadDataAndUpdate(); } else if ( auto viewColl = ensemble->viewCollection() ) { viewColl->removeView( view ); delete view; } } ensemble->updateConnectedEditors(); } } //-------------------------------------------------------------------------------------------------- /// Info text shown next to the realization filter. //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::realizationFilterInfoText() const { return "Available realizations: " + availableRealizationsRangeText(); } //-------------------------------------------------------------------------------------------------- /// Compact range text for the available ensemble realizations, e.g. "0-99". //-------------------------------------------------------------------------------------------------- QString RimSumoDataSource::availableRealizationsRangeText() const { std::vector intValues; for ( const auto& realizationId : m_availableRealizationIds() ) { bool ok = false; int value = realizationId.toInt( &ok ); if ( ok ) intValues.push_back( value ); } auto rangeString = RiaStdStringTools::formatRangeSelection( intValues ); return QString::fromStdString( rangeString ); }