Files
ResInsight/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.cpp
Jørgen Herje 52f9cdf777 Add Polygons data from Cloud (#14811)
Add fetch og polygons from cloud using ri-cloud-api service as submodule

Current version of submodule fetches polygons from Sumo, prodviding polygons with standard results tag.
2026-09-30 18:16:00 +02:00

753 lines
34 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2024 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimRoffCaseSumo.h"
#include "RiaApplication.h"
#include "RiaLogging.h"
#include "RiaPreferencesGrid.h"
#include "RiaResultNames.h"
#include "Cloud/RiaSumoConnector.h"
#include "Cloud/RimSumoDataSource.h"
#include "RifReaderSumoGridProperty.h"
#include "RifRoffFileTools.h"
#include "RigActiveCellInfo.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigEclipseResultInfo.h"
#include "RigMainGrid.h"
#include "RimCaseCollection.h"
#include "RimEclipseCellColors.h"
#include "RimEclipseView.h"
#include "RimReservoirCellResultsStorage.h"
#include "RimReservoirGridEnsembleBase.h"
#include "RiuMainWindow.h"
#include "RiuViewer.h"
#include "cafPdmObjectScriptingCapability.h"
#include "cafPdmPointer.h"
#include "cafProgressInfo.h"
#include <QDate>
#include <QDateTime>
#include <QStatusBar>
#include <algorithm>
#include <map>
#include <set>
#include <sstream>
#include <vector>
CAF_PDM_SOURCE_INIT( RimRoffCaseSumo, "RimRoffCaseSumo" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRoffCaseSumo::RimRoffCaseSumo()
{
CAF_PDM_InitScriptableObject( "Sumo Grid Case", ":/Case48x48.png" );
CAF_PDM_InitFieldNoDefault( &m_sumoDataSource, "SumoDataSource", "Sumo Data Source" );
m_sumoDataSource.uiCapability()->setUiHidden( true );
CAF_PDM_InitFieldNoDefault( &m_sumoCaseId, "SumoCaseId", "Sumo Case Id" );
m_sumoCaseId.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_ensembleName, "EnsembleName", "Ensemble Name" );
m_ensembleName.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitFieldNoDefault( &m_gridName, "GridName", "Grid Name" );
m_gridName.uiCapability()->setUiReadOnly( true );
CAF_PDM_InitField( &m_realization, "Realization", -1, "Realization" );
m_realization.uiCapability()->setUiReadOnly( true );
m_sumoConnector = RiaApplication::instance()->makeSumoConnector();
m_lifetimeToken = std::make_shared<bool>( true );
}
//--------------------------------------------------------------------------------------------------
/// Aborts any transfers this case still has in flight, see RiaSumoConnector::cancelGroup.
///
/// Rotate m_lifetimeToken before calling cancelGroup(), not after: an abort can make Qt deliver a
/// reply's finished() synchronously, re-entering this case's own completion lambdas while this
/// destructor is still on the stack. Those lambdas key off m_lifetimeToken.get() only to identify
/// which cancelGroup() call this transfer belongs to, not for an aliveness check (they rely on a
/// caf::PdmPointer for that) - but see RifReaderSumoGridProperty::~RifReaderSumoGridProperty() for
/// why this ordering still matters wherever a lifetime token is involved. Reset first, capture the
/// old raw pointer for the call, matching that fix for consistency/defense in depth.
//--------------------------------------------------------------------------------------------------
RimRoffCaseSumo::~RimRoffCaseSumo()
{
void* lifetimeTokenKey = m_lifetimeToken.get();
m_lifetimeToken.reset();
if ( m_sumoConnector ) m_sumoConnector->cancelGroup( lifetimeTokenKey );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRoffCaseSumo* RimRoffCaseSumo::createFromDataSource( RimSumoDataSource* dataSource, const QString& gridName, int realization )
{
if ( !dataSource ) return nullptr;
auto* gridCase = new RimRoffCaseSumo();
gridCase->setSumoDataSource( dataSource );
gridCase->setSumoCaseId( dataSource->caseId().get() );
gridCase->setEnsembleName( dataSource->ensembleName() );
gridCase->setGridName( gridName );
gridCase->setRealization( realization );
// The grid is stored on Sumo, not on disk, so there is no real grid file name. Still assign a unique
// synthetic one: RimReservoirGridEnsemble identifies its realizations by grid file name, see contains()
// and findByFileName().
gridCase->setGridFileName(
QString( "sumo/%1/%2/realization-%3/%4.roff" ).arg( dataSource->caseId().get() ).arg( gridName ).arg( realization ).arg( gridName ) );
// Name the case using grid name, asset, ensemble and realization, e.g. "Geogrid_Drogon_iter-0_Real_0".
QString caseDisplayName =
QString( "%1_%2_%3_Real_%4" ).arg( gridName, dataSource->assetName(), dataSource->ensembleName() ).arg( realization );
gridCase->setCustomCaseName( caseDisplayName );
return gridCase;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::setSumoDataSource( RimSumoDataSource* dataSource )
{
m_sumoDataSource = dataSource;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::setSumoCaseId( const QString& sumoCaseId )
{
m_sumoCaseId = sumoCaseId;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::setEnsembleName( const QString& ensembleName )
{
m_ensembleName = ensembleName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::setGridName( const QString& gridName )
{
m_gridName = gridName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::setRealization( int realization )
{
m_realization = realization;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRoffCaseSumo::gridName() const
{
return m_gridName();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RimRoffCaseSumo::realization() const
{
return m_realization();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSumoDataSource* RimRoffCaseSumo::dataSource() const
{
return m_sumoDataSource();
}
//--------------------------------------------------------------------------------------------------
/// Every realization downloads and parses its own grid blob, because a roff grid carries its geometry and
/// its active cells in one stream and there is no way to read the active cells alone. When the ensemble
/// already holds a grid of the same dimensions, the geometry just parsed is released and the shared grid
/// is used instead: shared grid mode keeps one RigMainGrid in memory for the whole ensemble rather than
/// one per realization. The active cells are always this realization's own, ACTNUM is never assumed to be
/// equal across realizations.
//--------------------------------------------------------------------------------------------------
bool RimRoffCaseSumo::openEclipseGridFile()
{
if ( eclipseCaseData() )
{
// Early exit if reservoir data is created
return true;
}
if ( !m_sumoConnector )
{
RiaLogging::error( "No Sumo connector available, unable to load grid from Sumo." );
return false;
}
setReservoirData( new RigEclipseCaseData( this ) );
// Before the grid, not after: the property transfer needs no grid to start, so the two run in parallel
// instead of one after the other.
startPropertyFetch();
// RifRoffFileTools::openGridFile appends into the grid it is given, so this must be a grid of our own.
// The shared grid can only be adopted once the parsing is done.
if ( !downloadAndParseGrid() )
{
// Leave no half built case behind, so opening this case can be retried after a failed transfer.
setReservoirData( nullptr );
return false;
}
RigMainGrid* parsedGrid = eclipseCaseData()->mainGrid();
RigMainGrid* gridToUse = parsedGrid;
QString gridOwnership = "own grid (case is not in a reservoir grid ensemble)";
RimReservoirGridEnsembleBase* gridEnsemble = parentCaseCollection() ? parentCaseCollection()->parentGridEnsembleBase() : nullptr;
if ( gridEnsemble )
{
gridToUse = gridEnsemble->shareOrAdoptMainGrid( parsedGrid );
gridOwnership = ( gridToUse != parsedGrid ) ? "shared grid"
: gridEnsemble->gridMode() == RimReservoirGridEnsembleBase::GridModeType::SHARED_GRID
? "own grid (published as the ensemble shared grid)"
: "own grid (ensemble uses individual grids)";
}
if ( gridToUse != parsedGrid )
{
// Point at the shared grid and release the geometry just parsed. The active cell info produced by
// the same parse belongs to this realization and is kept.
eclipseCaseData()->setMainGrid( gridToUse );
// The cell search tree, the bounding box and the faults of the shared grid were computed by the
// realization that loaded it, so computeCachedData() is not repeated here. The active cell bounding
// box is per realization and must be computed, as in RimEclipseResultCase::openAndReadActiveCellData.
computeActiveCellsBoundingBox();
eclipseCaseData()->setActiveFormationNames( effectiveFormationNames() );
}
else
{
parsedGrid->setFlipAxis( m_flipXAxis, m_flipYAxis );
computeCachedData();
}
// The same grid address for every realization, with the reference count climbing, is what shows the
// grid is shared. The active cell count is this realization's own, and is expected to vary between
// realizations even when they share a grid.
const auto* activeCells = eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
RiaLogging::debug( std::format( "Sumo grid '{}' realization {}: {} {}, {}x{}x{}, {} active cells, grid ref count {}",
m_gridName().toStdString(),
m_realization(),
gridOwnership.toStdString(),
static_cast<const void*>( gridToUse ),
gridToUse->cellCountI(),
gridToUse->cellCountJ(),
gridToUse->cellCountK(),
activeCells ? activeCells->reservoirActiveCellCount() : 0,
gridToUse->refCount() ) );
finalizeCaseSetup();
return true;
}
//--------------------------------------------------------------------------------------------------
/// The case can be closed and reopened many times while this object stays alive, e.g. when statistics for an
/// ensemble contour map release each realization's memory after processing it. m_propertyReader is a second
/// reference to the reader alongside the one held by the cell results: releasing it here is what lets the
/// reader (and its lifetime token) actually be destroyed when the case data is, instead of surviving with a
/// dangling m_caseData pointer that a Sumo transfer arriving later would write into.
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::closeReservoirCase()
{
m_propertyReader = nullptr;
cancelPendingTransfers();
RimEclipseCase::closeReservoirCase();
}
//--------------------------------------------------------------------------------------------------
/// Aborts the speculative fetch startPropertyFetch issues before the reader exists, see its cancelGroup
/// argument below. Without this, closing (or switching away from) this realization while that transfer is
/// still on its way leaves it running to completion, consuming a connection and bandwidth that a
/// newly selected realization's grid download needs. Unlike closeReservoirCase, this keeps already loaded
/// grid and result data intact, so switching back to this realization does not force a full reload.
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::cancelPendingTransfers()
{
m_fetchInFlight.reset();
// Rotate the token before cancelling, not after: see ~RimRoffCaseSumo for why a reentrant delivery
// during cancelGroup() must observe an already-superseded token, not the one about to be cancelled.
void* oldLifetimeTokenKey = m_lifetimeToken.get();
m_lifetimeToken = std::make_shared<bool>( true );
if ( m_sumoConnector ) m_sumoConnector->cancelGroup( oldLifetimeTokenKey );
}
//--------------------------------------------------------------------------------------------------
/// See header and RifReaderSumoGridProperty::prefetchDynamicResult.
//--------------------------------------------------------------------------------------------------
bool RimRoffCaseSumo::prefetchDynamicResult( const QString& resultName, const std::vector<size_t>& stepIndices )
{
if ( m_propertyReader.isNull() ) return false;
return m_propertyReader->prefetchDynamicResult( resultName, stepIndices );
}
//--------------------------------------------------------------------------------------------------
/// Download the roff grid blob for this realization and parse it into the case data.
//--------------------------------------------------------------------------------------------------
bool RimRoffCaseSumo::downloadAndParseGrid()
{
QByteArray contents = m_sumoConnector->grid().gridData( SumoCaseId( m_sumoCaseId() ), m_ensembleName(), m_gridName(), m_realization() );
if ( contents.isEmpty() )
{
RiaLogging::error(
std::format( "Failed to download grid '{}' (realization {}) from Sumo.", m_gridName().toStdString(), m_realization() ) );
return false;
}
// The downloaded blob is a binary roff grid. Parse it directly from memory.
std::string buffer = contents.toStdString();
std::istringstream stream( buffer, std::ios::binary );
QString errorMessages;
if ( !RifRoffFileTools::openGridFile( stream, eclipseCaseData(), &errorMessages ) )
{
RiaLogging::error( errorMessages.toStdString() );
return false;
}
return true;
}
//--------------------------------------------------------------------------------------------------
/// Register the results and attach the property reader. Shared by the two ways the grid can end up on
/// this case: parsed and kept, or parsed and replaced by the grid shared with the other realizations.
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::finalizeCaseSetup()
{
results( RiaDefines::PorosityModelType::MATRIX_MODEL )->createPlaceholderResultEntries();
if ( RiaPreferencesGrid::current()->autoComputeDepthRelatedProperties() )
{
eclipseCaseData()->computeDepthRelatedResults();
}
results( RiaDefines::PorosityModelType::MATRIX_MODEL )->computeCellVolumes();
// Make the Sumo grid properties available as cell results (fetched on demand when displayed).
registerSumoGridProperties();
// Rebuild the Data Sources result folders now that the result meta data is available.
updateResultAddressCollection();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRoffCaseSumo::locationOnDisc() const
{
// The grid is stored on Sumo, not on disk.
return QString();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
uiOrdering.add( &m_caseUserDescription );
uiOrdering.add( &m_displayNameOption );
uiOrdering.add( &m_caseId );
auto group = uiOrdering.addNewGroup( "Sumo" );
group->add( &m_sumoCaseId );
group->add( &m_ensembleName );
group->add( &m_gridName );
group->add( &m_realization );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::registerSumoGridProperties()
{
if ( !m_sumoConnector || !eclipseCaseData() ) return;
// Already fetched by startPropertyFetch in the normal case. Only ask again when it is not there, so a
// case opened without that step still works.
if ( m_propertyInfos.empty() )
{
m_propertyInfos = m_sumoConnector->grid().propertyInfo( SumoCaseId( m_sumoCaseId() ), m_ensembleName(), m_gridName(), m_realization() );
}
const auto& gridPropertyInfos = m_propertyInfos;
// Properties without a timestamp are static. Properties with a single timestamp are dynamic (one time
// step per timestamp). Time intervals (the iso string contains '/') are not supported and skipped.
std::vector<QString> staticPropertyNames;
std::map<QString, std::set<QString>> dynamicPropertyTimestamps; // property name -> the timestamps it has
std::set<QString> allTimestamps; // union of timestamps across all properties
for ( const auto& info : gridPropertyInfos )
{
if ( info.isoDateOrInterval.isEmpty() )
{
staticPropertyNames.push_back( info.name );
}
else if ( !info.isoDateOrInterval.contains( '/' ) )
{
dynamicPropertyTimestamps[info.name].insert( info.isoDateOrInterval );
allTimestamps.insert( info.isoDateOrInterval );
}
}
if ( staticPropertyNames.empty() && dynamicPropertyTimestamps.empty() ) return;
// The Eclipse readers take the available phases from the INIT file. There is no INIT file here, so derive
// the phases from the saturation properties the case actually has. Without this the phase set stays empty,
// and RigCaseCellResultsData::defaultResult() skips its SOIL and SGAS branches - both are guarded by the
// phase set, while the SWAT branch is not - so a new view would always open on SWAT.
auto hasProperty = [&]( const QString& name )
{ return dynamicPropertyTimestamps.contains( name ) || std::ranges::find( staticPropertyNames, name ) != staticPropertyNames.end(); };
std::set<RiaDefines::PhaseType> availablePhases;
if ( hasProperty( RiaResultNames::soil() ) ) availablePhases.insert( RiaDefines::PhaseType::OIL_PHASE );
if ( hasProperty( RiaResultNames::sgas() ) ) availablePhases.insert( RiaDefines::PhaseType::GAS_PHASE );
if ( hasProperty( RiaResultNames::swat() ) ) availablePhases.insert( RiaDefines::PhaseType::WATER_PHASE );
if ( !availablePhases.empty() ) eclipseCaseData()->setAvailablePhases( availablePhases );
auto cellResults = results( RiaDefines::PorosityModelType::MATRIX_MODEL );
if ( !cellResults ) return;
auto reader = new RifReaderSumoGridProperty( m_sumoConnector, m_sumoCaseId(), m_ensembleName(), m_gridName(), m_realization() );
reader->open( "", eclipseCaseData() );
// Register the property names as cell results so they are listed in the cell result editor. The values are
// not loaded here; the reader fetches them on demand the first time a property is displayed.
for ( const auto& name : staticPropertyNames )
{
RigEclipseResultAddress resultAddress( RiaDefines::ResultCatType::STATIC_NATIVE, RiaDefines::ResultDataType::FLOAT, name );
cellResults->createResultEntry( resultAddress, false );
}
reader->setStaticProperties( staticPropertyNames );
if ( !dynamicPropertyTimestamps.empty() )
{
auto parseTimestamp = []( const QString& isoString ) -> QDateTime
{
QDateTime dateTime = QDateTime::fromString( isoString, Qt::ISODate );
if ( !dateTime.isValid() )
{
// Date-only string, e.g. "2018-01-01".
QDate date = QDate::fromString( isoString, Qt::ISODate );
if ( date.isValid() ) dateTime = QDateTime( date, QTime( 0, 0, 0 ) );
}
return dateTime;
};
// All dynamic results share one common, case-wide set of time steps so they use the same time step
// index space as the 3D view time slider. std::set is sorted, and ISO date strings sort chronologically.
std::vector<QString> commonTimestamps( allTimestamps.begin(), allTimestamps.end() );
std::vector<QDateTime> dates;
std::vector<int> reportNumbers;
std::vector<double> daysSinceStart;
for ( int i = 0; i < static_cast<int>( commonTimestamps.size() ); i++ )
{
QDateTime date = parseTimestamp( commonTimestamps[i] );
dates.push_back( date );
reportNumbers.push_back( i );
daysSinceStart.push_back( ( dates.front().isValid() && date.isValid() ) ? dates.front().daysTo( date ) : static_cast<double>( i ) );
}
auto commonTimeStepInfos = RigEclipseTimeStepInfo::createTimeStepInfos( dates, reportNumbers, daysSinceStart );
// For each property, build a list aligned with commonTimestamps. An empty entry marks a time step the
// property has no data for, so the reader reports "no data" there instead of another step's values.
std::map<QString, std::vector<QString>> readerDynamicTimestamps;
for ( const auto& [name, propertyTimestamps] : dynamicPropertyTimestamps )
{
RigEclipseResultAddress resultAddress( RiaDefines::ResultCatType::DYNAMIC_NATIVE, RiaDefines::ResultDataType::FLOAT, name );
cellResults->createResultEntry( resultAddress, false );
cellResults->setTimeStepInfos( resultAddress, commonTimeStepInfos );
std::vector<QString> alignedTimestamps;
alignedTimestamps.reserve( commonTimestamps.size() );
for ( const auto& timestamp : commonTimestamps )
{
alignedTimestamps.push_back( propertyTimestamps.count( timestamp ) > 0 ? timestamp : QString() );
}
readerDynamicTimestamps[name] = alignedTimestamps;
}
reader->setDynamicProperties( readerDynamicTimestamps );
}
m_propertyReader = reader;
reader->setPendingChangedCallback( [this]( const QString& message ) { onPropertyPendingChanged( message ); } );
reader->setTimeStepArrivedCallback( [this]( const QString& propertyName, size_t stepIndex, bool ok )
{ onPropertyTimeStepArrived( propertyName, stepIndex, ok ); } );
reader->setBatchProgressChangedCallback( [this]( size_t completed, size_t total, const QString& description )
{ onPropertyBatchProgressChanged( completed, total, description ); } );
// The transfer started before this reader existed is still on its way. Tell the reader, so it reports the
// wait to the user and does not issue a second request for the same time step.
if ( m_fetchInFlight ) reader->markTimeStepPending( m_fetchInFlight->first, m_fetchInFlight->second );
cellResults->setReaderInterface( reader );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRoffCaseSumo::dataLoadingText() const
{
if ( m_propertyReader.isNull() ) return {};
const QString pending = m_propertyReader->pendingDataDescription();
if ( pending.isEmpty() ) return {};
return QString( "Loading %1 from Sumo" ).arg( pending );
}
//--------------------------------------------------------------------------------------------------
/// The case wide time step list: the union of the timestamps of every dynamic property, sorted. All dynamic
/// results share it, so it is the index space the 3D view time slider works in.
//--------------------------------------------------------------------------------------------------
static std::vector<QString> commonTimestampsFromPropertyInfo( const std::vector<SumoGridPropertyInfo>& infos )
{
std::set<QString> allTimestamps;
for ( const auto& info : infos )
{
// Time intervals are not supported, and a static property has no timestamp at all.
if ( info.isoDateOrInterval.isEmpty() || info.isoDateOrInterval.contains( '/' ) ) continue;
allTimestamps.insert( info.isoDateOrInterval );
}
return { allTimestamps.begin(), allTimestamps.end() };
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimRoffCaseSumo::propertyToFetch( QString& propertyName, size_t& stepIndex, QString& isoDateOrInterval ) const
{
// What a view of this case is about to show. Without one there is nothing to guess at, and the on demand
// path will fetch whatever is asked for soon enough.
for ( auto* view : reservoirViews() )
{
if ( !view || !view->cellResult() ) continue;
const QString candidate = view->cellResult()->resultVariable();
if ( candidate.isEmpty() ) continue;
const int currentStep = view->currentTimeStep();
if ( currentStep < 0 ) continue;
const auto timestamps = commonTimestampsFromPropertyInfo( m_propertyInfos );
if ( static_cast<size_t>( currentStep ) >= timestamps.size() ) continue;
// Only worth fetching when this property actually has data at that time step.
const QString candidateTimestamp = timestamps[currentStep];
const bool hasThisTimeStep =
std::ranges::any_of( m_propertyInfos,
[&]( const auto& info ) { return info.name == candidate && info.isoDateOrInterval == candidateTimestamp; } );
if ( !hasThisTimeStep ) continue;
propertyName = candidate;
stepIndex = static_cast<size_t>( currentStep );
isoDateOrInterval = candidateTimestamp;
return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::startPropertyFetch()
{
if ( !m_sumoConnector ) return;
// Small request, and the only thing here that needs waiting for. It also spares the later registration a
// second one.
m_propertyInfos = m_sumoConnector->grid().propertyInfo( SumoCaseId( m_sumoCaseId() ), m_ensembleName(), m_gridName(), m_realization() );
QString propertyName;
size_t stepIndex = 0;
QString isoDateOrInterval;
if ( !propertyToFetch( propertyName, stepIndex, isoDateOrInterval ) ) return;
m_fetchInFlight = std::make_pair( propertyName, stepIndex );
RiaLogging::debug( std::format( "Fetching '{}' (time '{}') for realization {} in parallel with the grid.",
propertyName.toStdString(),
isoDateOrInterval.toStdString(),
m_realization() ) );
// Auto-nulls if this case is deleted while the transfer is in flight.
caf::PdmPointer<RimRoffCaseSumo> self( const_cast<RimRoffCaseSumo*>( this ) );
// Tied to m_lifetimeToken so closeReservoirCase can abort this transfer, e.g. when the view switches away
// from this realization while it is still on its way, see closeReservoirCase.
m_sumoConnector->grid().propertyDataBatchAsync(
SumoCaseId( m_sumoCaseId() ),
m_ensembleName(),
m_gridName(),
m_realization(),
propertyName,
{ isoDateOrInterval },
[self, propertyName, stepIndex]( const QString& iso, const QByteArray& contents )
{
if ( self.isNull() || contents.isEmpty() ) return;
// The reader is attached while the grid download holds
// this thread on a semaphore, which dispatches no
// events, so it is here by the time this runs. If it is
// somehow not, the values are dropped and the on demand
// path fetches them again.
self->m_fetchInFlight.reset();
if ( self->m_propertyReader.notNull() )
{
self->m_propertyReader->acceptFetchedTimeStep( propertyName, stepIndex, iso, contents );
}
},
m_lifetimeToken.get() );
}
//--------------------------------------------------------------------------------------------------
/// A banner in the view itself, and the status bar. The status bar is easy to miss and the info text is
/// small and can be switched off, while the wait is measured in seconds.
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::onPropertyPendingChanged( const QString& message )
{
for ( auto* view : reservoirViews() )
{
if ( !view || !view->viewer() ) continue;
view->viewer()->setLoadingText( message );
view->viewer()->showLoadingLabel( !message.isEmpty() );
}
auto* mainWindow = RiuMainWindow::instance();
if ( !mainWindow || !mainWindow->statusBar() ) return;
if ( message.isEmpty() )
{
mainWindow->statusBar()->clearMessage();
}
else
{
mainWindow->statusBar()->showMessage( message );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::onPropertyTimeStepArrived( const QString& /*propertyName*/, size_t /*stepIndex*/, bool ok )
{
// A failed transfer leaves its placeholder in place; nothing new to show, so no redraw.
if ( !ok ) return;
scheduleRedrawOfViews();
}
//--------------------------------------------------------------------------------------------------
/// Blocks the GUI with a single progress dialog for as long as the reader has something pending, instead of
/// one per time step waited for in RifReaderSumoGridProperty::waitForTimeStepToArrive. The reader itself only
/// reports plain completed/total/description numbers via this callback - no GUI code lives there.
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::onPropertyBatchProgressChanged( size_t completed, size_t total, const QString& description )
{
if ( total == 0 )
{
// Whole batch done (or nothing was ever pending): close the dialog rather than leaving a "100%" one
// lingering, and forget what it was constructed with so the next batch starts a fresh dialog.
m_batchProgress.reset();
m_batchProgressShownTotal = 0;
return;
}
if ( !m_batchProgress || m_batchProgressShownTotal != total )
{
// Either nothing is open yet, or the total grew while a dialog was already open (e.g. a redraw
// reached a time step outside the original request): caf::ProgressInfo's maximum is fixed at
// construction, so a growing total needs a fresh dialog rather than resizing the existing one.
const bool delayShowingProgress = false;
m_batchProgress = std::make_unique<caf::ProgressInfo>( total, "Downloading Sumo grid property data", delayShowingProgress );
m_batchProgressShownTotal = total;
}
m_batchProgress->setProgress( completed );
m_batchProgress->setProgressDescription( QString( "Downloaded %1 of %2 time steps - %3" ).arg( completed ).arg( total ).arg( description ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRoffCaseSumo::scheduleRedrawOfViews()
{
// Redrawing reads cell results, which can start the next transfer and arrive back here through the
// reader's callback. Coalesce rather than recurse.
if ( m_isRedrawingViews )
{
m_hasMissedViewRedraw = true;
return;
}
m_isRedrawingViews = true;
do
{
m_hasMissedViewRedraw = false;
for ( auto* view : reservoirViews() )
{
if ( view ) view->scheduleCreateDisplayModelAndRedraw();
}
} while ( m_hasMissedViewRedraw );
m_isRedrawingViews = false;
}