#14880: RFT correlation plot formation workflow

* Add Add Well Picks button to well log formation settings
* Expose formation zone colors and shading alpha from RFT track
* Average observed pressures per formation with full min/max range
* Show formation-colored observed pressure and depth filter in RFT correlation plots
* Default new RFT correlation plots to formation filter and ensemble parameter coloring
* Move Qwt code out of RFT correlation Rim classes
* Align RFT correlation depth filter bars and click zones with displayed track formations
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2026-10-08 17:32:23 +02:00
1 parent 4126d25ea0
commit 5812826db6
22 files changed
+1179 -122

No files matched your search

@@ -37,6 +37,18 @@ CAF_CMD_SOURCE_INIT( RicImportWellFormationsFeature, "RicImportWellFormationsFea
///
//--------------------------------------------------------------------------------------------------
void RicImportWellFormationsFeature::onActionTriggered( bool isChecked )
{
std::vector<RimWellFormationsFile*> importedFiles = importFilesWithDialog();
if ( !importedFiles.empty() )
{
Riu3DMainWindowTools::selectAsCurrentItem( importedFiles.back() );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimWellFormationsFile*> RicImportWellFormationsFeature::importFilesWithDialog()
{
RiaApplication* app = RiaApplication::instance();
QString defaultDir = app->lastUsedDialogDirectory( "WELLPATHFORMATIONS_DIR" );
@@ -45,24 +57,21 @@ void RicImportWellFormationsFeature::onActionTriggered( bool isChecked )
defaultDir,
"Well Formations (*.csv);;All Files (*.*)" );
if ( filePaths.empty() ) return;
if ( filePaths.empty() ) return {};
// Remember the path for next time
app->setLastUsedDialogDirectory( "WELLPATHFORMATIONS_DIR", QFileInfo( filePaths.last() ).absolutePath() );
RimProject* project = RimProject::current();
if ( !project || !project->activeOilField() ) return;
if ( !project || !project->activeOilField() ) return {};
RimWellFormationsCollection* collection = project->activeOilField()->wellFormationsCollection();
if ( !collection ) return;
if ( !collection ) return {};
std::vector<RimWellFormationsFile*> importedFiles = collection->importFiles( filePaths );
collection->updateConnectedEditors();
if ( !importedFiles.empty() )
{
Riu3DMainWindowTools::selectAsCurrentItem( importedFiles.back() );
}
return importedFiles;
}
//--------------------------------------------------------------------------------------------------
@@ -20,6 +20,10 @@
#include "cafCmdFeature.h"
#include <vector>
class RimWellFormationsFile;
//==================================================================================================
/// Imports one or more well formations files (e.g. FMU formations.csv) into the project-level
/// RimWellFormationsCollection, without linking the resulting entries to any well path or file set.
@@ -28,6 +32,10 @@ class RicImportWellFormationsFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
public:
// Asks the user for files and imports them; returns the imported files without changing selection
static std::vector<RimWellFormationsFile*> importFilesWithDialog();
protected:
void onActionTriggered( bool isChecked ) override;
void setupActionLook( QAction* actionToSetup ) override;
@@ -150,6 +150,30 @@ void RimCurveSetAppearance::setEnsembleParameter( const QString& ensembleParamet
m_ensembleParameter = ensembleParameter;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimCurveSetAppearance::setColorByEnsembleParameter( const QString& ensembleParameter )
{
m_colorMode = RimEnsembleCurveSetColorManager::ColorMode::BY_ENSEMBLE_PARAM;
m_ensembleParameter = ensembleParameter;
updateEnsembleParameterLegend( m_ensembleLegendConfig );
}
//--------------------------------------------------------------------------------------------------
/// Changes the ensemble parameter only if curves are colored by ensemble parameter. Returns true
/// if the parameter was changed.
//--------------------------------------------------------------------------------------------------
bool RimCurveSetAppearance::syncEnsembleParameter( const QString& ensembleParameter )
{
if ( m_colorMode() != RimEnsembleCurveSetColorManager::ColorMode::BY_ENSEMBLE_PARAM || m_ensembleParameter() == ensembleParameter )
return false;
m_ensembleParameter = ensembleParameter;
updateEnsembleParameterLegend( m_ensembleLegendConfig );
return true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -61,6 +61,8 @@ public:
void setColorMode( ColorModeEnum colorMode );
void setEnsembleParameter( const QString& ensembleParameter );
bool syncEnsembleParameter( const QString& ensembleParameter );
void setColorByEnsembleParameter( const QString& ensembleParameter );
RimRegularLegendConfig* legendConfig() const;
private:
@@ -20,15 +20,20 @@
#include "Summary/RiaSummaryCurveDefinition.h"
#include "Formations/RimWellFormationsCollection.h"
#include "Formations/RimWellFormationsFile.h"
#include "RimCorrelationMatrixPlot.h"
#include "RimCorrelationPlot.h"
#include "RimCorrelationReportPlot.h"
#include "RimOilField.h"
#include "RimParameterResultCrossPlot.h"
#include "RimParameterRftCrossPlot.h"
#include "RimProject.h"
#include "RimRftCorrelationReportPlot.h"
#include "RimRftCrossPlotTools.h"
#include "RimSummaryEnsemble.h"
#include "RimSummaryEnsembleTools.h"
#include "RimWellRftEnsembleCurveSet.h"
#include "RimWellRftPlot.h"
CAF_PDM_SOURCE_INIT( RimCorrelationPlotCollection, "CorrelationPlotCollection" );
@@ -223,12 +228,32 @@ RimRftCorrelationReportPlot* RimCorrelationPlotCollection::createRftCorrelationR
if ( param.isNumeric() )
{
report->crossPlot()->setEnsembleParameter( param.name );
if ( auto* curveSet = report->wellRftPlot()->findEnsembleCurveSet( ensemble ) )
{
curveSet->setColorByEnsembleParameter( param.name );
}
break;
}
}
}
report->crossPlot()->setWellName( source->simWellOrWellPathName() );
// Default to filtering by formation when a formations file has data for the well
if ( auto* project = RimProject::current();
project && project->activeOilField() && project->activeOilField()->wellFormationsCollection() )
{
for ( RimWellFormationsFile* file : project->activeOilField()->wellFormationsCollection()->wellFormationsFiles() )
{
if ( file->formationsForWell( source->simWellOrWellPathName() ) )
{
report->crossPlot()->setWellFormations( file );
report->crossPlot()->setFilterMode( RimRftCrossPlotTools::DepthFilterMode::ZONES );
break;
}
}
}
const auto timeSteps = source->selectedTimeSteps();
if ( !timeSteps.empty() ) report->crossPlot()->setTimeStep( timeSteps.front() );
}
@@ -33,6 +33,7 @@
#include "RimEclipseResultCase.h"
#include "RimOilField.h"
#include "RimProject.h"
#include "RimRftCorrelationReportPlot.h"
#include "RimRftCrossPlotTools.h"
#include "RimSummaryCase.h"
#include "RimSummaryEnsemble.h"
@@ -40,24 +41,13 @@
#include "RiuContextMenuLauncher.h"
#include "RiuDockWidgetTools.h"
#include "RiuPlotCurve.h"
#include "RiuQwtCurveSelectorFilter.h"
#include "RiuQwtPlotCurve.h"
#include "RiuQwtPlotWidget.h"
#include "RiuQwtSymbol.h"
#include "RiuRftCorrelationPlotTools.h"
#include "cafPdmPointer.h"
#include "cafPdmUiComboBoxEditor.h"
#include "cafPdmUiTreeSelectionEditor.h"
#include "qwt_picker_machine.h"
#include "qwt_plot.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_marker.h"
#include "qwt_plot_picker.h"
#include "qwt_scale_map.h"
#include "qwt_text.h"
#include <QMouseEvent>
#include <QPaintDevice>
@@ -183,6 +173,30 @@ void RimParameterRftCrossPlot::setWellFormations( RimWellFormationsFile* wellFor
m_wellFormations = wellFormationsFile;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setFilterMode( RimRftCrossPlotTools::DepthFilterMode filterMode )
{
m_filterMode = filterMode;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setSelectedZones( const std::vector<QString>& zones )
{
m_selectedZones = zones;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setZoneColors( const std::map<QString, QColor>& zoneColors )
{
m_zoneColors = zoneColors;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -423,7 +437,7 @@ QString RimParameterRftCrossPlot::asciiDataForPlotExport() const
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::detachAllCurves()
{
if ( m_plotWidget ) m_plotWidget->qwtPlot()->detachItems();
if ( m_plotWidget ) RiuRftCorrelationPlotTools::detachAllItems( m_plotWidget );
}
//--------------------------------------------------------------------------------------------------
@@ -458,54 +472,6 @@ void RimParameterRftCrossPlot::doRenderWindowContent( QPaintDevice* paintDevice
if ( m_plotWidget ) m_plotWidget->render( paintDevice );
}
namespace
{
class CurveTracker : public QwtPlotPicker
{
public:
CurveTracker( QwtPlot* plot )
: QwtPlotPicker( plot->canvas() )
{
setStateMachine( new QwtPickerTrackerMachine() );
setRubberBand( QwtPicker::NoRubberBand );
setTrackerMode( QwtPicker::AlwaysOn );
}
protected:
QwtText trackerText( const QPoint& pos ) const override
{
double minDistance = std::numeric_limits<double>::max();
QString closestCurveLabel;
for ( QwtPlotItem* item : plot()->itemList() )
{
if ( item->rtti() == QwtPlotItem::Rtti_PlotCurve )
{
auto curve = static_cast<QwtPlotCurve*>( item );
double distance = std::numeric_limits<double>::max();
curve->closestPoint( pos, &distance );
if ( distance < minDistance )
{
minDistance = distance;
closestCurveLabel = curve->title().text();
}
}
}
if ( minDistance < 20.0 )
{
QwtText text( closestCurveLabel );
text.setBackgroundBrush( QBrush( Qt::white ) );
text.setColor( Qt::black );
return text;
}
return QwtText();
}
};
} // anonymous namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -520,12 +486,12 @@ RiuPlotWidget* RimParameterRftCrossPlot::doCreatePlotViewWidget( QWidget* parent
if ( m_plotWidget )
{
new CurveTracker( m_plotWidget->qwtPlot() );
RiuRftCorrelationPlotTools::installCurveTracker( m_plotWidget );
caf::PdmPointer<RimParameterRftCrossPlot> self( this );
new RiuQwtCurveSelectorFilter( m_plotWidget->qwtPlot(),
[self]( const QPoint& pos ) -> const caf::PdmUiItem*
{ return self ? self->findClosestCase( pos ) : nullptr; } );
RiuRftCorrelationPlotTools::installCurveSelector( m_plotWidget,
[self]( const QPoint& pos ) -> const caf::PdmUiItem*
{ return self ? self->findClosestCase( pos ) : nullptr; } );
}
return m_plotWidget;
@@ -549,15 +515,17 @@ void RimParameterRftCrossPlot::onLoadDataAndUpdate()
}
//--------------------------------------------------------------------------------------------------
///
/// Adds the data source, parameter and depth filter groups; used by the parent report plot, not shown
/// in the cross plot's own editor.
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
void RimParameterRftCrossPlot::appendDataAndFilterUiOrdering( caf::PdmUiOrdering& uiOrdering )
{
auto* dataGroup = uiOrdering.addNewGroup( "Data Source" );
dataGroup->add( &m_ensemble );
dataGroup->add( &m_wellName );
dataGroup->add( &m_selectedTimeStep );
dataGroup->add( &m_eclipseCase );
dataGroup->add( &m_ensembleParameter );
auto* depthGroup =
uiOrdering.addNewGroup( QString( "Depth Range (%1)" ).arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) );
@@ -575,9 +543,14 @@ void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmU
m_selectedZones.uiCapability()->setUiHidden( !useZones || !m_wellFormations() );
auto* crossPlotGroup = uiOrdering.addNewGroup( "Cross Plot Parameter" );
crossPlotGroup->add( &m_ensembleParameter );
crossPlotGroup->add( &m_samplingMode );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
auto* plotGroup = uiOrdering.addNewGroup( "Plot Settings" );
plotGroup->setCollapsedByDefault();
plotGroup->add( &m_useAutoPlotTitle );
@@ -622,7 +595,15 @@ void RimParameterRftCrossPlot::fieldChangedByUi( const caf::PdmFieldHandle* chan
}
RimPlot::fieldChangedByUi( changedField, oldValue, newValue );
loadDataAndUpdate();
// The parent report plot syncs the other sub plots and the track zone highlight from this plot
if ( auto* reportPlot = firstAncestorOrThisOfType<RimRftCorrelationReportPlot>() )
{
if ( changedField == &m_ensembleParameter ) reportPlot->syncEnsembleParameterToRftCurves();
reportPlot->loadDataAndUpdate();
}
else
loadDataAndUpdate();
}
//--------------------------------------------------------------------------------------------------
@@ -690,7 +671,7 @@ QList<caf::PdmOptionItemInfo> RimParameterRftCrossPlot::calculateValueOptions( c
if ( project && project->activeOilField() && project->activeOilField()->wellFormationsCollection() )
{
for ( RimWellFormationsFile* file : project->activeOilField()->wellFormationsCollection()->wellFormationsFiles() )
options.push_back( caf::PdmOptionItemInfo( file->shortName(), file ) );
options.push_back( caf::PdmOptionItemInfo( file->shortName(), file, false, file->uiCapability()->uiIconProvider() ) );
}
}
else if ( fieldNeedingOptions == &m_filterMode )
@@ -807,18 +788,65 @@ void RimParameterRftCrossPlot::createPoints()
{
const auto& [xValues, yValues] = pointsPerCase[summaryCase];
auto* plotCurve = new RiuQwtPlotCurve;
plotCurve->setSamplesValues( xValues, yValues );
plotCurve->setStyle( QwtPlotCurve::NoCurve );
RiuRftCorrelationPlotTools::PointSeries series;
series.x = xValues;
series.y = yValues;
series.title = summaryCase->displayCaseName();
series.color = colorTable.cycledQColor( idx++ );
series.isSelected = selectedSummaryCases.contains( summaryCase );
RiuRftCorrelationPlotTools::attachPointSeries( m_plotWidget, series );
}
const bool isSelected = selectedSummaryCases.contains( summaryCase );
auto* symbol = new RiuQwtSymbol( isSelected ? RiuPlotCurveSymbol::SYMBOL_XCROSS : RiuPlotCurveSymbol::SYMBOL_ELLIPSE );
symbol->setSize( 8, 8 );
symbol->setColor( colorTable.cycledQColor( idx++ ) );
plotCurve->setSymbol( symbol );
attachObservedPressure();
}
plotCurve->setTitle( summaryCase->displayCaseName() );
plotCurve->attach( m_plotWidget->qwtPlot() );
//--------------------------------------------------------------------------------------------------
/// Draws every observed pressure within the selected depth intervals as a solid horizontal line,
/// with dashed lines and a shaded band at +/- the observed error. Only the first line is labelled.
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::attachObservedPressure()
{
const auto observedPressures =
RimRftCrossPlotTools::computeObservedPressures( m_wellName(),
m_selectedTimeStep(),
depthIntervals(),
m_depthType(),
RimRftCrossPlotTools::buildAllZoneIntervals( m_wellFormations(),
m_wellName(),
m_depthType() ) );
if ( observedPressures.empty() ) return;
auto attachLine = [this]( double value, Qt::PenStyle style, const QString& label, const QColor& color )
{ RiuRftCorrelationPlotTools::attachHorizontalLine( m_plotWidget, value, style, label, color ); };
bool isFirst = true;
for ( const auto& observed : observedPressures )
{
// Use the formation color when known; otherwise fall back to black lines and no shaded band
std::optional<QColor> zoneColor;
if ( auto it = m_zoneColors.find( observed.zoneName ); it != m_zoneColors.end() && it->second.isValid() ) zoneColor = it->second;
QColor lineColor = zoneColor ? *zoneColor : QColor( Qt::black );
lineColor.setAlpha( 255 );
QString label;
if ( observed.count > 1 )
label = QString( "Observed Pressure (average of %1 observations)" ).arg( observed.count );
else if ( isFirst )
label = "Observed Pressure";
attachLine( observed.pressure, Qt::SolidLine, label, lineColor );
isFirst = false;
if ( observed.rangeMax <= observed.rangeMin ) continue;
attachLine( observed.rangeMin, Qt::DashLine, "", lineColor );
attachLine( observed.rangeMax, Qt::DashLine, "", lineColor );
if ( !zoneColor ) continue;
const QColor shadingColor = *zoneColor; // alpha matches the track's formation shading
RiuRftCorrelationPlotTools::attachHorizontalBand( m_plotWidget, observed.rangeMin, observed.rangeMax, shadingColor );
}
}
@@ -867,6 +895,13 @@ void RimParameterRftCrossPlot::updateValueRanges()
yMax = std::max( yMax, pressureValue );
}
for ( const auto& observed :
RimRftCrossPlotTools::computeObservedPressures( m_wellName(), m_selectedTimeStep(), depthIntervals(), m_depthType() ) )
{
yMin = std::min( yMin, observed.rangeMin );
yMax = std::max( yMax, observed.rangeMax );
}
if ( xMin == std::numeric_limits<double>::infinity() )
{
m_xValueRange = std::nullopt;
@@ -893,7 +928,7 @@ RimSummaryCase* RimParameterRftCrossPlot::findClosestCase( const QPoint& canvasP
for ( const auto& d : caseData )
points.push_back( { d.parameterValue, d.pressureValue } );
int idx = RiuQwtCurveSelectorFilter::closestPointIndex( m_plotWidget->qwtPlot(), canvasPos, points );
int idx = RiuRftCorrelationPlotTools::closestPointIndex( m_plotWidget, canvasPos, points );
return idx >= 0 ? caseData[idx].summaryCase : nullptr;
}
@@ -26,9 +26,11 @@
#include "cafPdmField.h"
#include "cafPdmPtrField.h"
#include <QColor>
#include <QDateTime>
#include <QPointer>
#include <map>
#include <optional>
#include <utility>
#include <vector>
@@ -77,6 +79,10 @@ public:
void setDepthType( RiaDefines::DepthType depthType );
void setEnsembleParameter( const QString& paramName );
void setWellFormations( RimWellFormationsFile* wellFormationsFile );
void setFilterMode( RimRftCrossPlotTools::DepthFilterMode filterMode );
void setSelectedZones( const std::vector<QString>& zones );
void setZoneColors( const std::map<QString, QColor>& zoneColors );
void appendDataAndFilterUiOrdering( caf::PdmUiOrdering& uiOrdering );
QString ensembleParameter() const;
QString wellName() const;
@@ -120,6 +126,7 @@ private:
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
void createPoints();
void attachObservedPressure();
void updatePlotTitle();
void updateValueRanges();
void cleanupBeforeClose();
@@ -137,6 +144,7 @@ private:
caf::PdmField<double> m_depthRangeMax;
caf::PdmPtrField<RimWellFormationsFile*> m_wellFormations;
caf::PdmField<std::vector<QString>> m_selectedZones;
std::map<QString, QColor> m_zoneColors;
caf::PdmField<caf::AppEnum<RiaDefines::DepthType>> m_depthType;
caf::PdmField<SamplingModeEnum> m_samplingMode;
caf::PdmField<QString> m_ensembleParameter;
@@ -18,16 +18,24 @@
#include "RimRftCorrelationReportPlot.h"
#include "RimColorLegend.h"
#include "RimColorLegendItem.h"
#include "RimDepthTrackPlot.h"
#include "RimParameterRftCrossPlot.h"
#include "RimRftCrossPlotTools.h"
#include "RimRftTornadoPlot.h"
#include "RimWellLogTrack.h"
#include "RimWellRftEnsembleCurveSet.h"
#include "RimWellRftPlot.h"
#include "Formations/RimWellFormationsFile.h"
#include "RiaWellLogUnitTools.h"
#include "RiuInterfaceToViewWindow.h"
#include "RiuPlotWidget.h"
#include "RiuQwtPlotWidget.h"
#include "RiuRftCorrelationPlotTools.h"
#include "DockAreaTitleBar.h"
#include "DockAreaWidget.h"
@@ -35,14 +43,24 @@
#include "DockWidget.h"
#include "cafPdmOptionItemInfo.h"
#include "cafPdmPointer.h"
#include "cafPdmUiCheckBoxEditor.h"
#include "cafPdmUiTreeOrdering.h"
#include "cafSelectionManager.h"
#include <QContextMenuEvent>
#include <QFrame>
#include <QMouseEvent>
#include <QPainter>
#include <QPointer>
#include <QSettings>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
#include <functional>
#include <optional>
static const char* RFT_DOCK_LAYOUT_REGISTRY_KEY = "RftCorrelationReportPlot/defaultDockLayout";
//--------------------------------------------------------------------------------------------------
@@ -253,6 +271,8 @@ void RimRftCorrelationReportPlot::recreatePlotWidgets()
if ( auto* w = m_tornadoPlot->viewer() ) w->installEventFilter( m_contextMenuFilter );
if ( auto* w = m_parameterRftCrossPlot->viewer() ) w->installEventFilter( m_contextMenuFilter );
installTrackClickFilters();
auto makeDockWidget = [&]( const QString& title, RimPlotWindow* plot, QWidget* widget ) -> ads::CDockWidget*
{
auto* dock = new ads::CDockWidget( title, m_dockManager );
@@ -402,6 +422,9 @@ void RimRftCorrelationReportPlot::onLoadDataAndUpdate()
if ( m_showWindow )
{
m_wellRftPlot->loadDataAndUpdate();
installTrackClickFilters();
updateSelectedZoneHighlight();
syncZoneColorsToCrossPlot();
syncTornadoInputsFromCrossPlot();
m_tornadoPlot->loadDataAndUpdate();
m_parameterRftCrossPlot->loadDataAndUpdate();
@@ -418,7 +441,7 @@ void RimRftCorrelationReportPlot::defineUiOrdering( QString uiConfigName, caf::P
uiOrdering.add( &m_depthType );
// Delegate cross-plot settings (ensemble, well, depth range, parameter) to the cross plot
m_parameterRftCrossPlot->uiOrdering( uiConfigName, uiOrdering );
m_parameterRftCrossPlot->appendDataAndFilterUiOrdering( uiOrdering );
auto* layoutGroup = uiOrdering.addNewGroup( "Dock Layout" );
layoutGroup->setCollapsedByDefault();
@@ -519,10 +542,27 @@ void RimRftCorrelationReportPlot::onTornadoParameterSelected( const QString& par
{
m_parameterRftCrossPlot->setEnsembleParameter( paramName );
m_parameterRftCrossPlot->loadDataAndUpdate();
syncEnsembleParameterToRftCurves();
}
updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
/// Makes RFT curves colored by ensemble parameter use the cross plot's ensemble parameter.
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::syncEnsembleParameterToRftCurves()
{
if ( !m_wellRftPlot() || !m_parameterRftCrossPlot() ) return;
auto* curveSet = m_wellRftPlot->findEnsembleCurveSet( m_parameterRftCrossPlot->ensemble() );
if ( curveSet && curveSet->syncEnsembleParameter( m_parameterRftCrossPlot->ensembleParameter() ) )
{
m_wellRftPlot->rebuildCurves();
curveSet->updateConnectedEditors();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -543,6 +583,135 @@ void RimRftCorrelationReportPlot::syncTornadoInputsFromCrossPlot()
m_tornadoPlot->setSelectedZones( m_parameterRftCrossPlot->selectedZones() );
}
//--------------------------------------------------------------------------------------------------
/// Makes a click on a formation in the RFT plot tracks select that formation as the zone filter
/// used by the cross plot and tornado plot.
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::installTrackClickFilters()
{
if ( !m_wellRftPlot() ) return;
if ( m_trackClickFilter ) m_trackClickFilter->deleteLater();
m_trackClickFilter = new QObject( this );
const auto orientation = m_wellRftPlot->depthOrientation();
const bool isVertical = orientation == RiaDefines::Orientation::VERTICAL;
for ( size_t i = 0; i < m_wellRftPlot->plotCount(); ++i )
{
auto* track = dynamic_cast<RimWellLogTrack*>( m_wellRftPlot->plotByIndex( i ) );
if ( !track ) continue;
RiuQwtPlotWidget* widget = track->viewer();
if ( !widget ) continue;
caf::PdmPointer<RimWellLogTrack> trackPtr( track );
RiuRftCorrelationPlotTools::installDepthClickFilter(
widget,
RimDepthTrackPlot::depthAxis( orientation ),
isVertical,
[this, trackPtr]( double depth )
{
if ( trackPtr ) onRftTrackDepthClicked( trackPtr, depth );
},
m_trackClickFilter );
}
}
//--------------------------------------------------------------------------------------------------
/// Marks the depth interval of each selected zone in the RFT plot tracks with a thin bar along the
/// depth axis side. Stale bars are found by type and removed before new ones are attached.
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::updateSelectedZoneHighlight()
{
if ( !m_wellRftPlot() || !m_parameterRftCrossPlot() ) return;
const auto filterMode = m_parameterRftCrossPlot->filterMode();
const bool isVertical = m_wellRftPlot->depthOrientation() == RiaDefines::Orientation::VERTICAL;
// Bars are drawn in plot coordinates, so the depth range is converted to the plot's depth unit
std::vector<RiuRftCorrelationPlotTools::DepthInterval> depthRangeBars;
if ( filterMode == RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE )
{
const auto fromDepthUnit = m_wellRftPlot->caseDepthUnit();
const auto toDepthUnit = m_wellRftPlot->depthUnit();
depthRangeBars.push_back(
{ RiaWellLogUnitTools<double>::convertDepth( m_parameterRftCrossPlot->depthRangeMin(), fromDepthUnit, toDepthUnit ),
RiaWellLogUnitTools<double>::convertDepth( m_parameterRftCrossPlot->depthRangeMax(), fromDepthUnit, toDepthUnit ) } );
}
const std::vector<QString> selectedZones = m_parameterRftCrossPlot->selectedZones();
for ( size_t i = 0; i < m_wellRftPlot->plotCount(); ++i )
{
auto* track = dynamic_cast<RimWellLogTrack*>( m_wellRftPlot->plotByIndex( i ) );
if ( !track || !track->viewer() ) continue;
std::vector<RiuRftCorrelationPlotTools::DepthInterval> barIntervals = depthRangeBars;
if ( filterMode == RimRftCrossPlotTools::DepthFilterMode::ZONES )
{
// Use the zones as shaded on this track so the bars line up with them
for ( const auto& [name, top, base] : track->formationZoneDisplayRanges() )
{
if ( std::find( selectedZones.begin(), selectedZones.end(), name ) != selectedZones.end() )
barIntervals.push_back( { top, base } );
}
}
RiuRftCorrelationPlotTools::setDepthIntervalBars( track->viewer(), barIntervals, isVertical );
}
}
//--------------------------------------------------------------------------------------------------
/// Gives the cross plot the formation colors used by the RFT plot tracks, looked up by formation name.
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::syncZoneColorsToCrossPlot()
{
if ( !m_wellRftPlot() || !m_parameterRftCrossPlot() ) return;
std::map<QString, QColor> zoneColors;
for ( size_t i = 0; i < m_wellRftPlot->plotCount() && zoneColors.empty(); ++i )
{
auto* track = dynamic_cast<RimWellLogTrack*>( m_wellRftPlot->plotByIndex( i ) );
if ( !track ) continue;
for ( const auto& [name, color] : track->formationZoneColors() )
{
// The alpha carries the track's shading transparency, used for the observed pressure band
zoneColors[name] = QColor( color.r(), color.g(), color.b(), track->formationShadingAlpha() );
}
}
m_parameterRftCrossPlot->setZoneColors( zoneColors );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::onRftTrackDepthClicked( RimWellLogTrack* track, double depth )
{
if ( !m_parameterRftCrossPlot() ) return;
// The track's zones are in plot coordinates and don't overlap, so the hit is the zone shaded at the click
for ( const auto& [name, top, base] : track->formationZoneDisplayRanges() )
{
if ( depth < std::min( top, base ) || depth > std::max( top, base ) ) continue;
std::vector<QString> zones = m_parameterRftCrossPlot->selectedZones();
auto it = std::find( zones.begin(), zones.end(), name );
if ( it != zones.end() )
zones.erase( it ); // clicking a selected zone again deselects it
else
zones.push_back( name );
m_parameterRftCrossPlot->setFilterMode( RimRftCrossPlotTools::DepthFilterMode::ZONES );
m_parameterRftCrossPlot->setSelectedZones( zones );
loadDataAndUpdate();
updateConnectedEditors();
return;
}
}
//--------------------------------------------------------------------------------------------------
/// Pushes the user's manual "Well Formations File" selection in the cross plot down to the sub
/// plots so their "Filter By: Zones" option becomes available.
@@ -31,6 +31,7 @@
class RimParameterRftCrossPlot;
class RimRftTornadoPlot;
class RimWellLogTrack;
class RimWellRftPlot;
namespace ads
@@ -66,6 +67,7 @@ public:
std::vector<RimPlot*> childPlotsForTextExport() const override;
void initializeFromSourcePlot( RimWellRftPlot* source );
void syncEnsembleParameterToRftCurves();
private:
QString createDescription() const;
@@ -91,6 +93,10 @@ private:
void syncTornadoInputsFromCrossPlot();
void applyDepthTypeToSubPlots();
void applyWellFormationsToSubPlots();
void installTrackClickFilters();
void updateSelectedZoneHighlight();
void syncZoneColorsToCrossPlot();
void onRftTrackDepthClicked( RimWellLogTrack* track, double depth );
private:
caf::PdmProxyValueField<QString> m_name;
@@ -106,6 +112,7 @@ private:
QWidget* m_viewWidget = nullptr;
QObject* m_contextMenuFilter = nullptr;
QObject* m_trackClickFilter = nullptr;
ads::CDockManager* m_dockManager = nullptr;
ads::CDockWidget* m_rftDockWidget = nullptr;
ads::CDockWidget* m_correlationDockWidget = nullptr;
@@ -24,17 +24,21 @@
#include "Well/RigEclipseWellLogExtractor.h"
#include "Well/RigWellPathFormations.h"
#include "Well/RigWellPathGeometryTools.h"
#include "Formations/RimWellFormationsFile.h"
#include "RimEclipseResultCase.h"
#include "RimObservedFmuRftData.h"
#include "RimProject.h"
#include "RimSummaryCase.h"
#include "RimSummaryEnsemble.h"
#include "RimWellPath.h"
#include "RimWellPlotTools.h"
#include <algorithm>
#include <limits>
#include <map>
#include <numeric>
namespace caf
@@ -80,9 +84,42 @@ std::vector<double> RimRftCrossPlotTools::rftCurveDepthValues( RifReaderRftInter
std::vector<double> depths;
reader->values( mdAddress, &depths );
if ( depths.empty() && extractor ) depths = reader->computeMeasuredDepth( wellName, timeStep, extractor );
if ( depths.empty() ) depths = measuredDepthFromObservedData( reader, wellName, timeStep );
return depths;
}
//--------------------------------------------------------------------------------------------------
/// Same fallback as RimWellLogRftCurve uses when there is no grid, so the filter sees the MD the
/// RFT curves are drawn at.
//--------------------------------------------------------------------------------------------------
std::vector<double>
RimRftCrossPlotTools::measuredDepthFromObservedData( RifReaderRftInterface* reader, const QString& wellName, const QDateTime& timeStep )
{
auto tvdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::TVD );
std::vector<double> tvdDepths;
reader->values( tvdAddress, &tvdDepths );
if ( tvdDepths.empty() ) return {};
auto mdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::MD );
for ( RimObservedFmuRftData* observedData : RimWellPlotTools::observedFmuRftDataForWell( wellName ) )
{
RifReaderRftInterface* observedReader = observedData->rftReader();
if ( !observedReader || !observedReader->availableTimeSteps( wellName ).count( timeStep ) ) continue;
std::vector<double> observedTvd;
std::vector<double> observedMd;
observedReader->values( tvdAddress, &observedTvd );
observedReader->values( mdAddress, &observedMd );
// At least two samples are needed to estimate the MD/TVD relationship
if ( observedTvd.size() < 2 || observedTvd.size() != observedMd.size() ) continue;
return RigWellPathGeometryTools::interpolateMdFromTvd( observedMd, observedTvd, tvdDepths );
}
return {};
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -93,7 +130,7 @@ std::vector<double> RimRftCrossPlotTools::filterPressuresByDepthRange( const std
double depthRangeMax )
{
std::vector<DepthInterval> intervals;
if ( useDepthRange ) intervals.push_back( { depthRangeMin, depthRangeMax } );
if ( useDepthRange ) intervals.push_back( { depthRangeMin, depthRangeMax, QString() } );
return filterPressuresByDepthIntervals( depths, pressures, intervals );
}
@@ -111,7 +148,7 @@ std::vector<RimRftCrossPlotTools::DepthInterval> RimRftCrossPlotTools::buildDept
{
if ( mode == DepthFilterMode::NONE ) return {};
if ( mode == DepthFilterMode::DEPTH_RANGE ) return { DepthInterval{ depthRangeMin, depthRangeMax } };
if ( mode == DepthFilterMode::DEPTH_RANGE ) return { DepthInterval{ depthRangeMin, depthRangeMax, QString() } };
// ZONES mode
if ( !wellFormationsFile || selectedZones.empty() ) return {};
@@ -128,14 +165,37 @@ std::vector<RimRftCrossPlotTools::DepthInterval> RimRftCrossPlotTools::buildDept
if ( !isSelected ) continue;
if ( depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH )
intervals.push_back( { formation.tvdTop, formation.tvdBase } );
intervals.push_back( { formation.tvdTop, formation.tvdBase, formation.formationName } );
else
intervals.push_back( { formation.mdTop, formation.mdBase } );
intervals.push_back( { formation.mdTop, formation.mdBase, formation.formationName } );
}
return intervals;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimRftCrossPlotTools::DepthInterval> RimRftCrossPlotTools::buildAllZoneIntervals( RimWellFormationsFile* wellFormationsFile,
const QString& wellName,
RiaDefines::DepthType depthType )
{
if ( !wellFormationsFile ) return {};
const RigWellPathFormations* formations = wellFormationsFile->formationsForWell( wellName );
if ( !formations ) return {};
const bool useTvd = depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH;
std::vector<DepthInterval> intervals;
for ( size_t i = 0; i < formations->formationCount(); ++i )
{
const RigWellPathFormation& formation = formations->formationAt( i );
intervals.push_back(
{ useTvd ? formation.tvdTop : formation.mdTop, useTvd ? formation.tvdBase : formation.mdBase, formation.formationName } );
}
return intervals;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -189,6 +249,105 @@ QString RimRftCrossPlotTools::depthFilterDescription( DepthFilterMode
return QString( "%1 %2 - %3 m" ).arg( depthTypeAbbreviation( depthType ) ).arg( depthRangeMin ).arg( depthRangeMax );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimRftCrossPlotTools::ObservedPressure>
RimRftCrossPlotTools::computeObservedPressures( const QString& wellName,
const QDateTime& timeStep,
const std::vector<DepthInterval>& depthIntervals,
RiaDefines::DepthType depthType,
const std::vector<DepthInterval>& zoneIntervals )
{
std::vector<ObservedPressure> result;
if ( wellName.isEmpty() || !timeStep.isValid() ) return result;
for ( RimObservedFmuRftData* observedData : RimWellPlotTools::observedFmuRftDataForWell( wellName ) )
{
RifReaderRftInterface* reader = observedData->rftReader();
if ( !reader || !reader->availableTimeSteps( wellName ).count( timeStep ) ) continue;
auto pressureAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::PRESSURE );
std::vector<double> pressures;
reader->values( pressureAddress, &pressures );
if ( pressures.empty() ) continue;
std::vector<double> depths = rftCurveDepthValues( reader, wellName, timeStep, nullptr, depthType );
auto errorAddress =
RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::PRESSURE_ERROR );
std::vector<double> errors;
reader->values( errorAddress, &errors );
// Filtering pressures and errors with the same depths/intervals keeps the two vectors aligned.
const std::vector<double> filteredPressures = filterPressuresByDepthIntervals( depths, pressures, depthIntervals );
std::vector<double> filteredErrors;
if ( errors.size() == pressures.size() ) filteredErrors = filterPressuresByDepthIntervals( depths, errors, depthIntervals );
const std::vector<double> filteredDepths = filterPressuresByDepthIntervals( depths, depths, depthIntervals );
for ( size_t i = 0; i < filteredPressures.size(); ++i )
{
const double error = i < filteredErrors.size() && filteredErrors.size() == filteredPressures.size() ? filteredErrors[i] : 0.0;
QString zoneName;
if ( filteredDepths.size() == filteredPressures.size() )
{
// When filtering by zones, name the sample by a selected zone so it is never shown as a zone outside the filter
const bool filteredByZones = !depthIntervals.empty() && !depthIntervals.front().zoneName.isEmpty();
const auto& lookup = filteredByZones || zoneIntervals.empty() ? depthIntervals : zoneIntervals;
double thickness = std::numeric_limits<double>::infinity();
for ( const auto& interval : lookup )
{
// Prefer the narrowest matching zone when formation levels overlap
if ( filteredDepths[i] >= interval.top && filteredDepths[i] <= interval.base && interval.base - interval.top < thickness )
{
zoneName = interval.zoneName;
thickness = interval.base - interval.top;
}
}
}
result.push_back( { filteredPressures[i], error, zoneName, filteredPressures[i] - error, filteredPressures[i] + error } );
}
}
// Combine multiple observations in the same zone: mean pressure, band spanning all observations
std::vector<ObservedPressure> aggregated;
std::vector<int> counts;
std::map<QString, size_t> zoneIndex;
for ( const auto& observed : result )
{
if ( observed.zoneName.isEmpty() )
{
aggregated.push_back( observed );
counts.push_back( 1 );
continue;
}
auto [it, inserted] = zoneIndex.insert( { observed.zoneName, aggregated.size() } );
if ( inserted )
{
aggregated.push_back( observed );
counts.push_back( 1 );
continue;
}
ObservedPressure& target = aggregated[it->second];
target.pressure += observed.pressure;
target.rangeMin = std::min( target.rangeMin, observed.rangeMin );
target.rangeMax = std::max( target.rangeMax, observed.rangeMax );
counts[it->second]++;
target.count++;
}
for ( size_t i = 0; i < aggregated.size(); ++i )
{
aggregated[i].pressure /= counts[i];
aggregated[i].error = std::max( aggregated[i].pressure - aggregated[i].rangeMin, aggregated[i].rangeMax - aggregated[i].pressure );
}
return aggregated;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -25,6 +25,7 @@
#include <QString>
#include <QStringList>
#include <optional>
#include <vector>
class RifReaderRftInterface;
@@ -54,25 +55,30 @@ using DepthFilterModeEnum = caf::AppEnum<DepthFilterMode>;
// A single inclusive depth interval, in the unit/datum of whichever depth type it was built for.
struct DepthInterval
{
double top = 0.0;
double base = 0.0;
double top = 0.0;
double base = 0.0;
QString zoneName; // set when the interval comes from a formation zone
};
// Short abbreviation used in axis titles and plot titles for the active depth type.
QString depthTypeAbbreviation( RiaDefines::DepthType depthType );
// Returns the depth values for the given well/time step, strictly matching depthType: MD
// (native RFT MD channel, falling back to extractor-derived MD if the channel is missing) or
// TVD (native RFT TVD channel). Returns an empty vector if the requested depth type is not
// available, rather than silently substituting the other depth type — callers that filter by
// depth range rely on this to exclude/blank out data instead of comparing a range against the
// wrong unit.
// (native RFT MD channel, falling back to extractor-derived MD, then to MD interpolated from
// observed FMU RFT data for the well) or TVD (native RFT TVD channel). Returns an empty vector if
// the requested depth type is not available, rather than silently substituting the other depth
// type — callers that filter by depth range rely on this to exclude/blank out data instead of
// comparing a range against the wrong unit.
std::vector<double> rftCurveDepthValues( RifReaderRftInterface* reader,
const QString& wellName,
const QDateTime& timeStep,
RigEclipseWellLogExtractor* extractor,
RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH );
// MD for the reader's TVD samples, interpolated from the MD/TVD pairs of observed FMU RFT data for
// the well at the time step. Empty if no such data is available.
std::vector<double> measuredDepthFromObservedData( RifReaderRftInterface* reader, const QString& wellName, const QDateTime& timeStep );
// Returns the subset of pressures whose corresponding depth is within [depthRangeMin, depthRangeMax].
// If useDepthRange is false, all pressures are returned unfiltered. If useDepthRange is true and
// depths/pressures are not the same size, an empty vector is returned (no aligned depth data).
@@ -93,6 +99,11 @@ std::vector<DepthInterval> buildDepthIntervals( DepthFilterMode mode
const std::vector<QString>& selectedZones,
RiaDefines::DepthType depthType );
// Returns one interval per formation zone of the well (named by zone), used to look up which zone a
// depth belongs to. Empty if no well formations file or no formations for the well.
std::vector<DepthInterval>
buildAllZoneIntervals( RimWellFormationsFile* wellFormationsFile, const QString& wellName, RiaDefines::DepthType depthType );
// Returns the subset of pressures whose corresponding depth lies within any of depthIntervals
// (inclusive). An empty depthIntervals list means "no filter", returning pressures unchanged; a
// size mismatch between depths/pressures returns an empty vector (no aligned depth data).
@@ -108,6 +119,27 @@ QString depthFilterDescription( DepthFilterMode mode,
double depthRangeMax,
const std::vector<QString>& selectedZones );
// A single observed (e.g. FMU) RFT pressure sample with its uncertainty.
struct ObservedPressure
{
double pressure = 0.0;
double error = 0.0; // observed pressure error; 0 if not available
QString zoneName; // formation zone containing the sample; empty if not filtered by zones
double rangeMin = 0.0; // lowest pressure - error among the observations
double rangeMax = 0.0; // highest pressure + error among the observations
int count = 1; // number of observations combined into this entry
};
// Observed pressure samples within depthIntervals for the well/time step, across all observed data
// sets. Samples in the same zone are combined into one entry: mean pressure, with rangeMin/rangeMax
// covering all of them. An empty depthIntervals list means no filtering. Samples are named by the
// selected zones when depthIntervals come from a zone filter, otherwise by zoneIntervals.
std::vector<ObservedPressure> computeObservedPressures( const QString& wellName,
const QDateTime& timeStep,
const std::vector<DepthInterval>& depthIntervals,
RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH,
const std::vector<DepthInterval>& zoneIntervals = {} );
// Arithmetic mean of the given samples, or infinity if samples is empty.
double computeMean( const std::vector<double>& samples );
@@ -36,12 +36,10 @@
#include "RiuGroupedBarChartBuilder.h"
#include "RiuPlotItem.h"
#include "RiuQwtPlotWidget.h"
#include "RiuRftCorrelationPlotTools.h"
#include "cafPdmUiCheckBoxEditor.h"
#include "qwt_plot.h"
#include "qwt_text.h"
#include <QPaintDevice>
#include <limits>
@@ -256,8 +254,7 @@ void RimRftTornadoPlot::detachAllCurves()
{
if ( m_plotWidget )
{
m_plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotBarChart );
m_plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotScale );
RiuRftCorrelationPlotTools::detachBarChartItems( m_plotWidget );
}
}
@@ -323,13 +320,13 @@ void RimRftTornadoPlot::onLoadDataAndUpdate()
m_lastCorrelations = addDataToChartBuilder( chartBuilder );
const int labelSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_labelFontSize() );
chartBuilder.setLabelFontSize( labelSize );
chartBuilder.addBarChartToPlot( m_plotWidget->qwtPlot(), Qt::Horizontal, m_showOnlyTopNCorrelations() ? m_topNFilterCount() : -1 );
RiuRftCorrelationPlotTools::addHorizontalBarChart( m_plotWidget, chartBuilder, m_showOnlyTopNCorrelations() ? m_topNFilterCount() : -1 );
RimCorrelationBarChartTools::highlightSelectedParameterBar( m_plotWidget,
m_selectedParameter,
RiaColorTools::toQColor( m_barColor() ),
RiaColorTools::toQColor( m_highlightBarColor() ) );
m_plotWidget->qwtPlot()->insertLegend( nullptr );
RiuRftCorrelationPlotTools::removeLegend( m_plotWidget );
updateAxes();
updatePlotTitle();
@@ -103,6 +103,22 @@ void RimWellRftEnsembleCurveSet::setCurveColor( const cvf::Color3f& color )
m_appearance->setMainEnsembleColor( color );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimWellRftEnsembleCurveSet::syncEnsembleParameter( const QString& ensembleParameter )
{
return m_appearance->syncEnsembleParameter( ensembleParameter );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWellRftEnsembleCurveSet::setColorByEnsembleParameter( const QString& ensembleParameter )
{
m_appearance->setColorByEnsembleParameter( ensembleParameter );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -48,6 +48,8 @@ public:
QString ensembleName() const;
void setCurveColor( const cvf::Color3f& color );
bool syncEnsembleParameter( const QString& ensembleParameter );
void setColorByEnsembleParameter( const QString& ensembleParameter );
cvf::Color3f curveColor( RimSummaryEnsemble* ensemble, const RimSummaryCase* summaryCase ) const;
RimRegularLegendConfig* legendConfig();
@@ -29,6 +29,8 @@
#include "RimWellLogTrack.h"
#include "RimWellPath.h"
#include "RicImportWellFormationsFeature.h"
#include "Formations/RimWellFormationsCollection.h"
#include "Formations/RimWellFormationsFile.h"
@@ -341,7 +343,10 @@ void RimWellLogFormationSettings::uiOrdering( const QString& uiConfigName, caf::
{
// Well picks use explicit top/base depths and do not require a well path trajectory, so
// this source can be used e.g. for an RFT well with no associated/modelled well path.
uiOrdering.add( &m_wellFormationsFile );
uiOrdering.add( &m_wellFormationsFile, { .newRow = true, .totalColumnSpan = 2, .leftLabelColumnSpan = 1 } );
uiOrdering.addNewButton( "Add Well Picks",
[this]() { onAddWellPicksClicked(); },
{ .newRow = false, .totalColumnSpan = 1, .leftLabelColumnSpan = 0 } );
if ( m_wellFormationsFile() ) uiOrdering.add( &m_wellNameInFormationsFile );
if ( resolveWellPickFormations() )
@@ -393,6 +398,27 @@ void RimWellLogFormationSettings::fieldChangedByUi( const caf::PdmFieldHandle* c
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimWellLogFormationSettings::onAddWellPicksClicked()
{
std::vector<RimWellFormationsFile*> importedFiles = RicImportWellFormationsFeature::importFilesWithDialog();
if ( importedFiles.empty() ) return;
RimWellFormationsFile* file = importedFiles.back();
m_wellFormationsFile = file;
QStringList wellNames = file->wellNames();
m_wellNameInFormationsFile = wellNames.isEmpty() ? QString() : wellNames.first();
if ( auto track = firstAncestorOrThisOfType<RimWellLogTrack>() )
{
track->loadDataAndUpdate();
track->updateEditors();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -106,6 +106,7 @@ protected:
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
private:
void onAddWellPicksClicked();
caf::PdmField<caf::AppEnum<RiaDefines::WellLogTrackFormationSource>> m_formationSource;
caf::PdmPtrField<RimCase*> m_formationCase;
caf::PdmField<caf::AppEnum<RiaDefines::WellLogTrackTrajectoryType>> m_formationTrajectoryType;
@@ -40,7 +40,7 @@ RimWellLogRegionAnnotationSettings::RimWellLogRegionAnnotationSettings()
CAF_PDM_InitFieldNoDefault( &m_colorShadingLegend, "ColorShadingLegend", "Colors" );
m_colorShadingLegend = RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::NORMAL );
CAF_PDM_InitField( &m_colorShadingTransparency, "ColorShadingTransparency", 50, "Color Transparency" );
CAF_PDM_InitField( &m_colorShadingTransparency, "ColorShadingTransparency", 85, "Color Transparency" );
m_colorShadingTransparency.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() );
CAF_PDM_InitField( &m_showRegionLabels, "ShowFormationLabels", true, "Show Labels" );
@@ -1587,8 +1587,9 @@ void RimWellLogTrack::updateParentPlotZoom()
void RimWellLogTrack::updateEditors()
{
updateConnectedEditors();
RimPlotWindow* plotWindow = firstAncestorOrThisOfTypeAsserted<RimPlotWindow>();
plotWindow->updateConnectedEditors();
// Also refresh the owning plot window (e.g. the RFT plot that embeds the track fields in its own UI)
if ( auto plotWindow = firstAncestorOfType<RimPlotWindow>() ) plotWindow->updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
@@ -1780,6 +1781,104 @@ RiuQwtPlotWidget* RimWellLogTrack::viewer()
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RimWellLogTrack::formationShadingAlpha() const
{
if ( !m_regionAnnotationSettings ) return 255;
return ( ( 100 - m_regionAnnotationSettings->colorShadingTransparency() ) * 255 ) / 100;
}
//--------------------------------------------------------------------------------------------------
/// Well pick formation zones as drawn on the track: at the track's formation level, with RKB offset
/// and in the plot's depth unit.
//--------------------------------------------------------------------------------------------------
std::vector<std::tuple<QString, double, double>> RimWellLogTrack::formationZoneDisplayRanges() const
{
auto* plot = firstAncestorOrThisOfType<RimDepthTrackPlot>();
if ( !plot ) return {};
if ( m_formationSettings->formationSource() != RiaDefines::WellLogTrackFormationSource::WELL_PICK_FILTER &&
m_formationSettings->formationSource() != RiaDefines::WellLogTrackFormationSource::WELL_PICKS_NO_TRAJECTORY )
{
return {};
}
auto formations = m_formationSettings->resolveWellPickFormations();
if ( !formations ) return {};
double rkbDiff = 0.0;
if ( plot->depthType() == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH_RKB )
{
// Picks-only wells have no trajectory
auto sourceWellPath = m_formationSettings->wellPathForSourceWellPath();
if ( sourceWellPath && sourceWellPath->wellPathGeometry() )
{
rkbDiff = sourceWellPath->wellPathGeometry()->rkbDiff();
}
}
const auto fromDepthUnit = plot->caseDepthUnit();
const auto toDepthUnit = plot->depthUnit();
const auto formationLevel = static_cast<RigWellPathFormations::FormationLevel>( m_formationSettings->formationLevel() );
auto depthRanges = formations->depthRangesUpToLevel( formationLevel, plot->depthType() );
for ( auto& [name, top, base] : depthRanges )
{
top = RiaWellLogUnitTools<double>::convertDepth( top + rkbDiff, fromDepthUnit, toDepthUnit );
base = RiaWellLogUnitTools<double>::convertDepth( base + rkbDiff, fromDepthUnit, toDepthUnit );
}
return depthRanges;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::map<QString, cvf::Color3ub> RimWellLogTrack::formationZoneColors() const
{
std::map<QString, cvf::Color3ub> colors;
auto* plot = firstAncestorOrThisOfType<RimDepthTrackPlot>();
if ( !plot || !m_regionAnnotationSettings ) return colors;
if ( m_formationSettings->formationSource() != RiaDefines::WellLogTrackFormationSource::WELL_PICK_FILTER &&
m_formationSettings->formationSource() != RiaDefines::WellLogTrackFormationSource::WELL_PICKS_NO_TRAJECTORY )
{
return colors;
}
auto formations = m_formationSettings->resolveWellPickFormations();
if ( !formations ) return colors;
RimColorLegend* legend = m_regionAnnotationSettings->colorShadingLegend();
if ( !legend ) legend = RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::NORMAL );
if ( !legend ) return colors;
std::map<QString, cvf::Color3ub> nameToColor;
for ( auto* item : legend->colorLegendItems() )
{
nameToColor[item->categoryName()] = cvf::Color3ub( item->color() );
}
// Same name-first, palette-index-second assignment as updateFormationNamesOnPlot()
const cvf::Color3ubArray paletteColors = legend->colorArray();
const auto formationLevel = static_cast<RigWellPathFormations::FormationLevel>( m_formationSettings->formationLevel() );
const auto depthRanges = formations->depthRangesUpToLevel( formationLevel, plot->depthType() );
for ( size_t i = 0; i < depthRanges.size(); i++ )
{
const QString& name = std::get<0>( depthRanges[i] );
auto it = nameToColor.find( name );
if ( it != nameToColor.end() )
colors.emplace( name, it->second );
else if ( paletteColors.size() > 0 )
colors.emplace( name, paletteColors[i % paletteColors.size()] );
}
return colors;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -2615,8 +2714,6 @@ void RimWellLogTrack::updateFormationNamesOnPlot()
return;
}
auto formationLevel = static_cast<RigWellPathFormations::FormationLevel>( m_formationSettings->formationLevel() );
double rkbDiff = 0.0;
if ( plot->depthType() == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH_RKB )
{
@@ -2643,20 +2740,17 @@ void RimWellLogTrack::updateFormationNamesOnPlot()
// Formations have a top/base depth range, so they can be drawn as shaded zones (or plain
// lines) using the same region-display machinery as the CASE-based formation names below.
auto depthRanges = formations->depthRangesUpToLevel( formationLevel, plot->depthType() );
auto depthRanges = formationZoneDisplayRanges();
if ( !depthRanges.empty() )
{
std::vector<QString> zoneNamesToPlot;
std::vector<std::pair<double, double>> zoneYValues;
std::vector<std::pair<double, double>> convertedZoneYValues;
for ( const auto& [name, top, base] : depthRanges )
{
zoneNamesToPlot.push_back( name );
zoneYValues.emplace_back( top + rkbDiff, base + rkbDiff );
convertedZoneYValues.emplace_back( top, base );
}
std::vector<std::pair<double, double>> convertedZoneYValues =
RiaWellLogUnitTools<double>::convertDepths( zoneYValues, fromDepthUnit, toDepthUnit );
// Build color table ordered by formation name to ensure correct color mapping
// when using a legend based on a LYR-file. Falls back to palette index for
// formations not found in the legend (e.g., when using a generic color palette).
@@ -35,10 +35,13 @@
#include "cafPdmObject.h"
#include "cafPdmPtrField.h"
#include "cvfColor3.h"
#include <QPointer>
#include <map>
#include <memory>
#include <tuple>
#include <vector>
namespace RiaDefines
@@ -98,9 +101,12 @@ public:
RimWellLogTrack();
~RimWellLogTrack() override;
QWidget* viewWidget() override;
RiuQwtPlotWidget* viewer();
RiuPlotWidget* plotWidget() override;
QWidget* viewWidget() override;
RiuQwtPlotWidget* viewer();
std::map<QString, cvf::Color3ub> formationZoneColors() const;
std::vector<std::tuple<QString, double, double>> formationZoneDisplayRanges() const;
int formationShadingAlpha() const;
RiuPlotWidget* plotWidget() override;
void zoomAll() override;
@@ -89,6 +89,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/Riu3DMainWindowTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuDockWidgetTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuQwtCurveSelectorFilter.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuRftCorrelationPlotTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotItemGroup.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiuDepthQwtPlot.cpp
@@ -0,0 +1,352 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiuRftCorrelationPlotTools.h"
#include "RiuGroupedBarChartBuilder.h"
#include "RiuPlotCurve.h"
#include "RiuQwtCurveSelectorFilter.h"
#include "RiuQwtPlotCurve.h"
#include "RiuQwtPlotWidget.h"
#include "RiuQwtSymbol.h"
#include "qwt_picker_machine.h"
#include "qwt_plot.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_marker.h"
#include "qwt_plot_picker.h"
#include "qwt_plot_zoneitem.h"
#include "qwt_scale_map.h"
#include "qwt_text.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPointer>
#include <QTimer>
#include <algorithm>
#include <cmath>
#include <limits>
#include <optional>
namespace
{
//--------------------------------------------------------------------------------------------------
/// Shows the title of the curve closest to the mouse cursor.
//--------------------------------------------------------------------------------------------------
class CurveTracker : public QwtPlotPicker
{
public:
CurveTracker( QwtPlot* plot )
: QwtPlotPicker( plot->canvas() )
{
setStateMachine( new QwtPickerTrackerMachine() );
setRubberBand( QwtPicker::NoRubberBand );
setTrackerMode( QwtPicker::AlwaysOn );
}
protected:
QwtText trackerText( const QPoint& pos ) const override
{
double minDistance = std::numeric_limits<double>::max();
QString closestCurveLabel;
for ( QwtPlotItem* item : plot()->itemList() )
{
if ( item->rtti() == QwtPlotItem::Rtti_PlotCurve )
{
auto curve = static_cast<QwtPlotCurve*>( item );
double distance = std::numeric_limits<double>::max();
curve->closestPoint( pos, &distance );
if ( distance < minDistance )
{
minDistance = distance;
closestCurveLabel = curve->title().text();
}
}
}
if ( minDistance < 20.0 )
{
QwtText text( closestCurveLabel );
text.setBackgroundBrush( QBrush( Qt::white ) );
text.setColor( Qt::black );
return text;
}
return QwtText();
}
};
//--------------------------------------------------------------------------------------------------
/// Thin gray translucent bar drawn in pixel width along the left edge (vertical depth axis) or
/// bottom edge (horizontal depth axis) of the canvas, spanning a depth interval.
//--------------------------------------------------------------------------------------------------
class DepthIntervalBar : public QwtPlotItem
{
public:
DepthIntervalBar( double top, double base, bool isDepthVertical )
: m_top( top )
, m_base( base )
, m_isDepthVertical( isDepthVertical )
{
setZ( 5.0 );
}
void draw( QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap, const QRectF& canvasRect ) const override
{
constexpr double barWidth = 6.0;
QRectF bar;
if ( m_isDepthVertical )
{
const double y0 = yMap.transform( m_top );
const double y1 = yMap.transform( m_base );
bar = QRectF( canvasRect.left(), std::min( y0, y1 ), barWidth, std::abs( y1 - y0 ) );
}
else
{
const double x0 = xMap.transform( m_top );
const double x1 = xMap.transform( m_base );
bar = QRectF( std::min( x0, x1 ), canvasRect.bottom() - barWidth, std::abs( x1 - x0 ), barWidth );
}
painter->fillRect( bar, QColor( 90, 90, 90, 110 ) );
}
private:
double m_top;
double m_base;
bool m_isDepthVertical;
};
//--------------------------------------------------------------------------------------------------
/// Reports the depth value under a left-button click (not drag) on a depth track canvas.
//--------------------------------------------------------------------------------------------------
class DepthClickFilter : public QObject
{
public:
DepthClickFilter( QwtPlot* plot, QwtAxisId depthAxis, bool isDepthVertical, std::function<void( double )> callback, QObject* parent )
: QObject( parent )
, m_plot( plot )
, m_depthAxis( depthAxis )
, m_isDepthVertical( isDepthVertical )
, m_callback( std::move( callback ) )
{
}
bool eventFilter( QObject*, QEvent* event ) override
{
if ( !m_plot ) return false;
if ( event->type() == QEvent::MouseButtonPress )
{
auto* mouseEvent = static_cast<QMouseEvent*>( event );
if ( mouseEvent->button() == Qt::LeftButton ) m_pressPos = mouseEvent->pos();
}
else if ( event->type() == QEvent::MouseButtonRelease )
{
auto* mouseEvent = static_cast<QMouseEvent*>( event );
if ( mouseEvent->button() == Qt::LeftButton && m_pressPos && ( mouseEvent->pos() - *m_pressPos ).manhattanLength() < 4 )
{
const double pixel = m_isDepthVertical ? mouseEvent->pos().y() : mouseEvent->pos().x();
const double depth = m_plot->invTransform( m_depthAxis, pixel );
// Defer so the model and plot updates run after this mouse event is fully handled
QTimer::singleShot( 0, this, [this, depth]() { m_callback( depth ); } );
}
m_pressPos.reset();
}
return false;
}
private:
QPointer<QwtPlot> m_plot;
QwtAxisId m_depthAxis;
bool m_isDepthVertical;
std::function<void( double )> m_callback;
std::optional<QPoint> m_pressPos;
};
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::detachAllItems( RiuQwtPlotWidget* plotWidget )
{
if ( plotWidget ) plotWidget->qwtPlot()->detachItems();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::attachPointSeries( RiuQwtPlotWidget* plotWidget, const PointSeries& series )
{
auto* plotCurve = new RiuQwtPlotCurve;
plotCurve->setSamplesValues( series.x, series.y );
plotCurve->setStyle( QwtPlotCurve::NoCurve );
auto* symbol = new RiuQwtSymbol( series.isSelected ? RiuPlotCurveSymbol::SYMBOL_XCROSS : RiuPlotCurveSymbol::SYMBOL_ELLIPSE );
symbol->setSize( 8, 8 );
symbol->setColor( series.color );
plotCurve->setSymbol( symbol );
plotCurve->setTitle( series.title );
plotCurve->attach( plotWidget->qwtPlot() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::attachHorizontalLine( RiuQwtPlotWidget* plotWidget,
double yValue,
Qt::PenStyle style,
const QString& label,
const QColor& color )
{
auto* marker = new QwtPlotMarker();
marker->setLineStyle( QwtPlotMarker::HLine );
marker->setYValue( yValue );
QPen pen( color );
pen.setStyle( style );
pen.setWidth( 1 );
marker->setLinePen( pen );
if ( !label.isEmpty() )
{
QwtText text( label );
text.setColor( Qt::black );
marker->setLabel( text );
marker->setLabelAlignment( Qt::AlignTop | Qt::AlignLeft );
}
marker->setZ( 1000.0 );
marker->attach( plotWidget->qwtPlot() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::attachHorizontalBand( RiuQwtPlotWidget* plotWidget, double minValue, double maxValue, const QColor& color )
{
auto* shading = new QwtPlotZoneItem();
shading->setOrientation( Qt::Horizontal );
shading->setInterval( minValue, maxValue );
shading->setPen( color, 0.0, Qt::NoPen );
shading->setBrush( QBrush( color ) );
shading->setZ( 999.0 );
shading->attach( plotWidget->qwtPlot() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::installCurveTracker( RiuQwtPlotWidget* plotWidget )
{
new CurveTracker( plotWidget->qwtPlot() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::installCurveSelector( RiuQwtPlotWidget* plotWidget,
std::function<const caf::PdmUiItem*( const QPoint& )> finder )
{
new RiuQwtCurveSelectorFilter( plotWidget->qwtPlot(), std::move( finder ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiuRftCorrelationPlotTools::closestPointIndex( RiuQwtPlotWidget* plotWidget,
const QPoint& canvasPos,
const std::vector<std::pair<double, double>>& points )
{
return RiuQwtCurveSelectorFilter::closestPointIndex( plotWidget->qwtPlot(), canvasPos, points );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::installDepthClickFilter( RiuQwtPlotWidget* plotWidget,
const RiuPlotAxis& depthAxis,
bool isDepthVertical,
std::function<void( double )> callback,
QObject* filterParent )
{
if ( !plotWidget || !plotWidget->qwtPlot() ) return;
auto* filter =
new DepthClickFilter( plotWidget->qwtPlot(), plotWidget->toQwtPlotAxis( depthAxis ), isDepthVertical, std::move( callback ), filterParent );
plotWidget->qwtPlot()->canvas()->installEventFilter( filter );
}
//--------------------------------------------------------------------------------------------------
/// Stale bars are found by type and removed before the new ones are attached.
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::setDepthIntervalBars( RiuQwtPlotWidget* plotWidget,
const std::vector<DepthInterval>& intervals,
bool isDepthVertical )
{
if ( !plotWidget ) return;
QwtPlot* qwtPlot = plotWidget->qwtPlot();
if ( !qwtPlot ) return;
// itemList() is a live reference; copy it before deleting items
const QwtPlotItemList items = qwtPlot->itemList();
for ( QwtPlotItem* item : items )
{
if ( dynamic_cast<DepthIntervalBar*>( item ) ) delete item;
}
for ( const auto& interval : intervals )
{
auto* bar = new DepthIntervalBar( interval.top, interval.base, isDepthVertical );
bar->attach( qwtPlot );
}
qwtPlot->replot();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::addHorizontalBarChart( RiuQwtPlotWidget* plotWidget, RiuGroupedBarChartBuilder& chartBuilder, int maxBarCount )
{
chartBuilder.addBarChartToPlot( plotWidget->qwtPlot(), Qt::Horizontal, maxBarCount );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::detachBarChartItems( RiuQwtPlotWidget* plotWidget )
{
if ( !plotWidget ) return;
plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotBarChart );
plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotScale );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiuRftCorrelationPlotTools::removeLegend( RiuQwtPlotWidget* plotWidget )
{
plotWidget->qwtPlot()->insertLegend( nullptr );
}
@@ -0,0 +1,84 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <QColor>
#include <QString>
#include <functional>
#include <utility>
#include <vector>
class QObject;
class QPoint;
class RiuGroupedBarChartBuilder;
class RiuPlotAxis;
class RiuQwtPlotWidget;
namespace caf
{
class PdmUiItem;
}
//==================================================================================================
/// Qwt specific plot operations used by the RFT correlation plots, kept out of the Rim* classes.
//==================================================================================================
namespace RiuRftCorrelationPlotTools
{
struct PointSeries
{
std::vector<double> x;
std::vector<double> y;
QString title;
QColor color;
bool isSelected = false;
};
struct DepthInterval
{
double top;
double base;
};
void detachAllItems( RiuQwtPlotWidget* plotWidget );
void attachPointSeries( RiuQwtPlotWidget* plotWidget, const PointSeries& series );
void attachHorizontalLine( RiuQwtPlotWidget* plotWidget, double yValue, Qt::PenStyle style, const QString& label, const QColor& color );
void attachHorizontalBand( RiuQwtPlotWidget* plotWidget, double minValue, double maxValue, const QColor& color );
// Shows the title of the closest curve under the mouse cursor
void installCurveTracker( RiuQwtPlotWidget* plotWidget );
// Selects the item returned by finder when the user clicks near a curve point
void installCurveSelector( RiuQwtPlotWidget* plotWidget, std::function<const caf::PdmUiItem*( const QPoint& )> finder );
int closestPointIndex( RiuQwtPlotWidget* plotWidget, const QPoint& canvasPos, const std::vector<std::pair<double, double>>& points );
// Reports the depth under a left-button click on the plot canvas. The filter is owned by filterParent.
void installDepthClickFilter( RiuQwtPlotWidget* plotWidget,
const RiuPlotAxis& depthAxis,
bool isDepthVertical,
std::function<void( double )> callback,
QObject* filterParent );
// Replaces the thin bars marking depth intervals along the depth axis side of the plot
void setDepthIntervalBars( RiuQwtPlotWidget* plotWidget, const std::vector<DepthInterval>& intervals, bool isDepthVertical );
void addHorizontalBarChart( RiuQwtPlotWidget* plotWidget, RiuGroupedBarChartBuilder& chartBuilder, int maxBarCount );
void detachBarChartItems( RiuQwtPlotWidget* plotWidget );
void removeLegend( RiuQwtPlotWidget* plotWidget );
} // namespace RiuRftCorrelationPlotTools