Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 195 additions & 3 deletions ApplicationLibCode/Application/Tools/Cloud/RiaOsduConnector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,77 @@
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QRegularExpression>

#include <limits>

#include "cafAssert.h"

namespace
{
//--------------------------------------------------------------------------------------------------
/// Parse an OSDU UnitOfMeasure reference id (e.g. "data:reference-data--UnitOfMeasure:ft:") and return
/// the multiplier that converts the value into meters. Returns 1.0 (and sets recognized=false) when the
/// id is empty or the symbol is unknown, so callers can log a single warning at the call site.
//--------------------------------------------------------------------------------------------------
double unitOfMeasureToMeters( const QString& unitId, bool* recognized = nullptr )
{
if ( recognized ) *recognized = false;

QString symbol;
if ( unitId.endsWith( ':' ) )
{
int lastColon = unitId.lastIndexOf( ':', unitId.length() - 2 );
if ( lastColon >= 0 ) symbol = unitId.mid( lastColon + 1, unitId.length() - lastColon - 2 );
}
if ( symbol.isEmpty() ) return 1.0;

if ( symbol.compare( "m", Qt::CaseInsensitive ) == 0 )
{
if ( recognized ) *recognized = true;
return 1.0;
}
if ( symbol.compare( "ft", Qt::CaseInsensitive ) == 0 )
{
if ( recognized ) *recognized = true;
return 0.3048;
}
return 1.0;
}

//--------------------------------------------------------------------------------------------------
/// Extract the linear unit factor (-> meters) from a persistableReferenceCrs JSON string by scanning
/// the embedded WKT. The PROJCS WKT places the projection's linear UNIT after the nested angular GEOGCS
/// UNIT, so the last `UNIT["name", factor]` token is the linear one. Falls back to 1.0 on any parse
/// failure or for non-projected CRSs.
//--------------------------------------------------------------------------------------------------
double linearCrsUnitToMeters( const QString& persistableReferenceCrs )
{
if ( persistableReferenceCrs.isEmpty() ) return 1.0;

QJsonDocument doc = QJsonDocument::fromJson( persistableReferenceCrs.toUtf8() );
if ( !doc.isObject() ) return 1.0;

QJsonObject obj = doc.object();
QString wkt = obj["wkt"].toString();
if ( wkt.isEmpty() ) wkt = obj["lateBoundCRS"].toObject()["wkt"].toString();
if ( wkt.isEmpty() ) return 1.0;
if ( !wkt.startsWith( "PROJCS", Qt::CaseInsensitive ) ) return 1.0;

QRegularExpression re( "UNIT\\[\"[^\"]+\"\\s*,\\s*([0-9.eE+\\-]+)\\]" );
auto matches = re.globalMatch( wkt );
double factor = 1.0;
while ( matches.hasNext() )
{
QRegularExpressionMatch m = matches.next();
bool ok = false;
double v = m.captured( 1 ).toDouble( &ok );
if ( ok ) factor = v;
}
return factor;
}
} // namespace

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -79,6 +145,7 @@ void RiaOsduConnector::clearCachedData()
m_wellLogs.clear();
m_parquetData.clear();
m_parquetErrors.clear();
m_wellSurfaceLocations.clear();
}

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -382,8 +449,16 @@ void RiaOsduConnector::parseWellboresByFieldId( QNetworkReply* reply, const QStr
QString verticalMeasurementId = vma["VerticalMeasurementID"].toString();
if ( verticalMeasurementId == defaultVerticalMeasurementId )
{
double verticalMeasurement = vma["VerticalMeasurement"].toDouble( 0.0 );
datumElevation = verticalMeasurement;
double verticalMeasurement = vma["VerticalMeasurement"].toDouble( 0.0 );
QString unitId = vma["VerticalMeasurementUnitOfMeasureID"].toString();
bool unitRecognized = false;
double factor = unitOfMeasureToMeters( unitId, &unitRecognized );
if ( !unitRecognized && !unitId.isEmpty() )
{
RiaLogging::warning(
QString( "Unrecognized datum elevation unit '%1' for well bore '%2'; assuming meters." ).arg( unitId ).arg( name ) );
}
datumElevation = verticalMeasurement * factor;
}
}

Expand All @@ -393,6 +468,8 @@ void RiaOsduConnector::parseWellboresByFieldId( QNetworkReply* reply, const QStr
datumElevation = 0.0;
}

// Wellbore records typically do not carry SpatialLocation; the surface point lives on the parent
// Well record. Surface easting/northing/crs are populated separately via requestWellSurfaceLocationBlocking.
m_wellbores[fieldId].push_back( OsduWellbore{ id, kind, name, wellId, fieldId, datumElevation } );
}
}
Expand Down Expand Up @@ -430,6 +507,7 @@ void RiaOsduConnector::parseWellTrajectory( QNetworkReply* reply, const QString&
QString id = resultObj["id"].toString();
QString kind = resultObj["kind"].toString();
QString existenceKind;
QString crs;

// Safely extract existenceKind from nested data object
QJsonObject dataObj = resultObj["data"].toObject();
Expand All @@ -438,7 +516,40 @@ void RiaOsduConnector::parseWellTrajectory( QNetworkReply* reply, const QString&
existenceKind = dataObj["ExistenceKind"].toString();
}

m_wellboreTrajectories[wellboreId].push_back( OsduWellboreTrajectory{ id, kind, wellboreId, existenceKind } );
QJsonObject spatialLocation = dataObj["SpatialLocation"].toObject();
QJsonObject ingested = spatialLocation["AsIngestedCoordinates"].toObject();
if ( !ingested.isEmpty() )
{
crs = ingested["persistableReferenceCrs"].toString();
}

// The MD entry in AvailableTrajectoryStationProperties advertises a length unit that the parquet
// values are stored in. Treat it as the canonical length unit for the geometric columns
// (MD/TVD/X/Y) and use it to derive a multiplier into meters, so downstream code can combine the
// trajectory with surface origin and datum elevation (which are stored as meters).
//
// Note: in real-world OSDU records the X/Y entries sometimes advertise a different unit than
// MD/TVD (e.g. "dega" while the values are clearly meters/feet). Trusting MD's unit and applying
// it uniformly to all four columns gives the right result for those datasets too.
QString mdUnitId;
QJsonArray availableProps = dataObj["AvailableTrajectoryStationProperties"].toArray();
for ( const QJsonValue& propValue : availableProps )
{
QJsonObject propObj = propValue.toObject();
if ( propObj["Name"].toString() == "MD" )
{
mdUnitId = propObj["StationPropertyUnitID"].toString();
break;
}
}
bool unitRecognized = false;
double unitToMeters = unitOfMeasureToMeters( mdUnitId, &unitRecognized );
if ( !unitRecognized && !mdUnitId.isEmpty() )
{
RiaLogging::warning( QString( "Unrecognized MD unit '%1' for trajectory %2; assuming meters." ).arg( mdUnitId ).arg( id ) );
}

m_wellboreTrajectories[wellboreId].push_back( OsduWellboreTrajectory{ id, kind, wellboreId, existenceKind, crs, unitToMeters } );
}
}

Expand Down Expand Up @@ -750,3 +861,84 @@ void RiaOsduConnector::cancelRequestForId( const QString& id )
}
}
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaOsduConnector::WellSurfaceLocation RiaOsduConnector::requestWellSurfaceLocationBlocking( const QString& wellId )
{
if ( wellId.isEmpty() ) return {};

// OSDU stores record references with a trailing colon (e.g. "data:master-data--Well:abcd:"). The storage
// API expects the id without it.
QString recordId = wellId;
while ( recordId.endsWith( ':' ) )
recordId.chop( 1 );

{
QMutexLocker lock( &m_mutex );
auto it = m_wellSurfaceLocations.find( recordId );
if ( it != m_wellSurfaceLocations.end() ) return it->second;
}

QString token = requestTokenBlocking();
QString url = m_server + "/api/storage/v2/records/" + recordId;

QNetworkRequest networkRequest;
networkRequest.setUrl( QUrl( url ) );
addStandardHeader( networkRequest, token, m_dataPartitionId, RiaCloudDefines::contentTypeJson() );

QNetworkReply* reply = m_networkAccessManager->get( networkRequest );

QEventLoop loop;
connect( reply, &QNetworkReply::finished, &loop, &QEventLoop::quit );
loop.exec();

WellSurfaceLocation location;

if ( reply->error() == QNetworkReply::NoError )
{
QByteArray body = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson( body );
QJsonObject data = doc.object()["data"].toObject();
QJsonObject spatialLocation = data["SpatialLocation"].toObject();
QJsonObject ingested = spatialLocation["AsIngestedCoordinates"].toObject();

if ( !ingested.isEmpty() )
{
location.crs = ingested["persistableReferenceCrs"].toString();
const double crsToMeters = linearCrsUnitToMeters( location.crs );
QJsonArray features = ingested["features"].toArray();
if ( !features.isEmpty() )
{
QJsonArray coordinates = features[0].toObject()["geometry"].toObject()["coordinates"].toArray();
if ( coordinates.size() >= 2 )
{
// CRS WKT may declare a non-meter linear unit (e.g. US survey foot for state-plane CRSs).
// Convert here so downstream code can rely on the surface origin always being meters.
location.easting = coordinates[0].toDouble() * crsToMeters;
location.northing = coordinates[1].toDouble() * crsToMeters;
location.isValid = true;
}
}
}

if ( !location.isValid )
{
RiaLogging::warning( QString( "No SpatialLocation found on Well record '%1'." ).arg( recordId ) );
}
}
else
{
RiaLogging::error( QString( "Failed to download Well record '%1': %2" ).arg( recordId ).arg( reply->errorString() ) );
}

reply->deleteLater();

{
QMutexLocker lock( &m_mutex );
m_wellSurfaceLocations[recordId] = location;
}

return location;
}
15 changes: 15 additions & 0 deletions ApplicationLibCode/Application/Tools/Cloud/RiaOsduConnector.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ struct OsduWellbore
QString wellId;
QString fieldId;
double datumElevation;
double surfaceEasting = 0.0;
double surfaceNorthing = 0.0;
QString crs;
};

struct OsduWellboreTrajectory
Expand All @@ -51,6 +54,8 @@ struct OsduWellboreTrajectory
QString kind;
QString wellboreId;
QString existenceKind;
QString crs;
double unitToMeters = 1.0;
};

struct OsduWellLogChannel
Expand Down Expand Up @@ -106,6 +111,15 @@ class RiaOsduConnector : public RiaCloudConnector
std::pair<QByteArray, QString> requestWellLogParquetDataByIdBlocking( const QString& wellLogId );
std::pair<QByteArray, QString> requestWellboreTrajectoryParquetDataByIdBlocking( const QString& wellboreTrajectoryId );

struct WellSurfaceLocation
{
double easting = 0.0;
double northing = 0.0;
QString crs;
bool isValid = false;
};
WellSurfaceLocation requestWellSurfaceLocationBlocking( const QString& wellId );

std::optional<OsduWellbore> wellboreById( const QString& wellboreId ) const;

void cancelRequestForId( const QString& id );
Expand Down Expand Up @@ -174,4 +188,5 @@ private slots:
std::map<QString, QByteArray> m_parquetData;
std::map<QString, QString> m_parquetErrors;
std::map<QString, QPointer<QNetworkReply>> m_replies;
std::map<QString, WellSurfaceLocation> m_wellSurfaceLocations;
};
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ void RicWellPathsImportOsduFeature::onActionTriggered( bool isChecked )
wellPath->setWellboreTrajectoryId( w.wellboreTrajectoryId );
wellPath->setExistenceKind( w.existenceKind );
wellPath->setDatumElevationFromOsdu( w.datumElevation );
wellPath->setSurfaceEastingFromOsdu( w.surfaceEasting );
wellPath->setSurfaceNorthingFromOsdu( w.surfaceNorthing );
wellPath->setCrsFromOsdu( w.crs );
wellPath->setUnitToMetersFromOsdu( w.unitToMeters );
wellPath->setTargetUnitToMeters( w.targetUnitToMeters );
wellPath->setWellPathColor( RiaColorTables::wellPathsPaletteColors().cycledColor3f( colorIndex++ ) );

newWells.push_back( wellPath );
Expand Down
48 changes: 46 additions & 2 deletions ApplicationLibCode/Commands/OsduCommands/RiuWellImportWizard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@

#include "RiuWellImportWizard.h"

#include "RiaDefines.h"
#include "RiaStringListSerializer.h"

#include "RigEclipseCaseData.h"

#include "RimEclipseCase.h"
#include "RimProject.h"

#include <QAbstractTableModel>
#include <QObject>
#include <QString>
Expand Down Expand Up @@ -596,6 +602,36 @@ WellSummaryPage::WellSummaryPage( RiaOsduConnector* osduConnector, QWidget* pare

layout->addLayout( existenceFilterLayout );

// Target unit system: the OSDU trajectory parquet is converted to this unit before storing the well path.
// Default to the unit system of the first grid case in the project so the imported well path matches the
// case's coordinate frame; fall back to Meters when no Eclipse case is loaded.
QHBoxLayout* unitLayout = new QHBoxLayout;
unitLayout->addWidget( new QLabel( "Target unit system:", this ) );
m_targetUnitComboBox = new QComboBox( this );
m_targetUnitComboBox->addItem( "Meters", 1.0 );
m_targetUnitComboBox->addItem( "Feet", 0.3048 );

int defaultUnitIndex = 0;
if ( auto* project = RimProject::current() )
{
for ( RimCase* gridCase : project->allGridCases() )
{
auto* eclipseCase = dynamic_cast<RimEclipseCase*>( gridCase );
if ( eclipseCase && eclipseCase->eclipseCaseData() )
{
if ( eclipseCase->eclipseCaseData()->unitsType() == RiaDefines::EclipseUnitSystem::UNITS_FIELD )
{
defaultUnitIndex = 1;
}
break;
}
}
}
m_targetUnitComboBox->setCurrentIndex( defaultUnitIndex );
unitLayout->addWidget( m_targetUnitComboBox );
unitLayout->addStretch();
layout->addLayout( unitLayout );

m_textEdit = new QTextEdit( this );
m_textEdit->setReadOnly( true );
layout->addWidget( m_textEdit );
Expand All @@ -608,6 +644,7 @@ WellSummaryPage::WellSummaryPage( RiaOsduConnector* osduConnector, QWidget* pare

connect( m_showAllRadioButton, SIGNAL( toggled( bool ) ), this, SLOT( onFilterChanged() ) );
connect( m_showActualRadioButton, SIGNAL( toggled( bool ) ), this, SLOT( onFilterChanged() ) );
connect( m_targetUnitComboBox, SIGNAL( currentIndexChanged( int ) ), this, SLOT( onFilterChanged() ) );
}

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -712,13 +749,20 @@ void WellSummaryPage::updateSummaryDisplay()
{
if ( shouldIncludeTrajectory( w.existenceKind ) )
{
QString wellboreTrajectoryId = w.id;
QString wellboreTrajectoryId = w.id;
auto location = m_osduConnector->requestWellSurfaceLocationBlocking( wellbore.value().wellId );
const double targetUnitToMeters = m_targetUnitComboBox->currentData().toDouble();
wiz->addWellInfo( { .name = wellbore.value().name,
.wellId = wellbore.value().wellId,
.wellboreId = w.wellboreId,
.wellboreTrajectoryId = wellboreTrajectoryId,
.existenceKind = w.existenceKind,
.datumElevation = wellbore.value().datumElevation } );
.datumElevation = wellbore.value().datumElevation,
.surfaceEasting = location.easting,
.surfaceNorthing = location.northing,
.crs = location.crs,
.unitToMeters = w.unitToMeters,
.targetUnitToMeters = targetUnitToMeters } );
Comment on lines 750 to +765

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Wizard surface fetch deadlock 🐞 Bug ☼ Reliability

WellSummaryPage::updateSummaryDisplay holds m_mutex while calling
requestWellSurfaceLocationBlocking(), which runs a nested QEventLoop that processes user input; a
re-entrant onFilterChanged()/updateSummaryDisplay will attempt to re-lock m_mutex and freeze the
wizard. The surface-location request also has no timeout, so the nested loop can block indefinitely
on slow/hung networking.
Agent Prompt
### Issue description
`WellSummaryPage::updateSummaryDisplay()` holds `m_mutex` while calling `requestWellSurfaceLocationBlocking()`, which runs a nested `QEventLoop` that processes user input events and has no timeout. This can deadlock on re-entrancy (e.g., `currentIndexChanged` firing while the loop is running) and can hang indefinitely.

### Issue Context
`RiaCloudConnector::requestTokenBlocking()` already demonstrates a safer approach with a `QTimer` timeout and `ExcludeUserInputEvents`.

### Fix Focus Areas
- ApplicationLibCode/Commands/OsduCommands/RiuWellImportWizard.cpp[704-741]
- ApplicationLibCode/Application/Tools/Cloud/RiaOsduConnector.cpp[803-831]
- ApplicationLibCode/Application/Tools/Cloud/RiaCloudConnector.cpp[346-360]

### Suggested fix
- Do not call blocking network I/O while holding `WellSummaryPage::m_mutex`:
  - Copy the needed `m_wellboreTrajectories` data to a local structure under lock, then release the lock before any network calls.
- Add a timeout + exclude user input for the nested loop in `requestWellSurfaceLocationBlocking()` (mirror `requestTokenBlocking()`):
  - Use a `QTimer` to quit the loop after a sensible timeout.
  - Call `loop.exec(QEventLoop::ExcludeUserInputEvents)` to prevent UI-triggered re-entrancy.
- Consider making surface-location retrieval async (preferred), but the above changes are the minimal safe fix.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

includedCount++;
}
}
Expand Down
Loading
Loading