Files
ResInsight/ApplicationLibCode/FileInterface/RifWellPathFormationReader.cpp
T
Magne Sjaastad 3fea49441d Use consistent formation colors in RFT plots based on formations file order
Palette colors for formations missing from the legend were assigned by position in the per-well zone list, so the same formation could get different colors for different wells. Assign them by order of first appearance in the formations file instead.
2026-10-15 18:01:46 +02:00

190 lines
7.4 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2017- Statoil 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 "RifWellPathFormationReader.h"
#include "RifCsvHtmlTableTools.h"
#include <QFile>
#include <QStringList>
#include <QTextStream>
#include <optional>
#include <vector>
namespace
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QStringList parseHeader( const QString& line, QChar delimiter )
{
QString header = line.toLower();
header.removeIf( []( QChar c ) { return c.isSpace() || c == '_'; } );
return header.split( delimiter );
}
//--------------------------------------------------------------------------------------------------
/// Returns the index of the first of the candidate column names present in the header, or -1
//--------------------------------------------------------------------------------------------------
int findColumn( const QStringList& header, std::initializer_list<const char*> candidates )
{
for ( const char* candidate : candidates )
{
const int index = header.indexOf( QString( candidate ) );
if ( index != -1 ) return index;
}
return -1;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<double> toOptionalDouble( const QStringList& columns, int index )
{
if ( index == -1 || index >= columns.size() ) return std::nullopt;
bool ok = false;
const double value = columns[index].toDouble( &ok );
return ok ? std::optional( value ) : std::nullopt;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RifWellPathFormationReader::WellFormations, QString> RifWellPathFormationReader::readWellFormations( const QString& filePath,
QStringList* zoneOrder )
{
QFile file( filePath );
if ( !file.open( QFile::ReadOnly | QFile::Text ) )
{
return std::unexpected( QString( "Failed to open %1" ).arg( filePath ) );
}
return parseWellFormations( QString::fromUtf8( file.readAll() ), filePath, zoneOrder );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RifWellPathFormationReader::WellFormations, QString>
RifWellPathFormationReader::parseWellFormations( const QString& content, const QString& filePath, QStringList* zoneOrder )
{
const auto parseFailure = QString( "Failed to parse %1 as a well pick file" ).arg( filePath );
QString text = content;
QTextStream stream( &text );
QStringList header;
QChar delimiter = ';';
while ( header.size() < 3 )
{
if ( stream.atEnd() ) return std::unexpected( parseFailure );
const QString rawHeaderLine = stream.readLine();
delimiter = RifCsvHtmlTableTools::detectDelimiter( rawHeaderLine );
header = parseHeader( rawHeaderLine, delimiter );
}
// The "well pick" formats use wellname/unitname, the FMU formations.csv format uses well/zone (and
// ignores zone_code). Both support md and/or tvd, and the FMU format can also include an x/y
// position for the zone top.
//
// "tvdss" columns hold a positive magnitude in the opposite (RMS, Z-up) sign convention and are
// negated to match ResInsight's positive-down TVD. Plain "tvd" columns (as used in the FMU
// formations.csv) are already positive-down and are used as-is.
const int wellNameIndex = findColumn( header, { "wellname", "well" } );
const int unitNameIndex = findColumn( header, { "unitname", "zone" } );
const int mdTopIndex = header.indexOf( "topmd" );
const int mdBaseIndex = header.indexOf( "basemd" );
const int tvdSSTopIndex = header.indexOf( "toptvdss" );
const int tvdSSBaseIndex = header.indexOf( "basetvdss" );
const int tvdTopIndex = tvdSSTopIndex != -1 ? tvdSSTopIndex : header.indexOf( "toptvd" );
const int tvdBaseIndex = tvdSSBaseIndex != -1 ? tvdSSBaseIndex : header.indexOf( "basetvd" );
const int xIndex = header.indexOf( "xutme" );
const int yIndex = header.indexOf( "yutmn" );
const bool hasMd = mdTopIndex != -1 && mdBaseIndex != -1;
const bool hasTvd = tvdTopIndex != -1 && tvdBaseIndex != -1;
const bool isTvdSS = tvdSSTopIndex != -1 && tvdSSBaseIndex != -1;
const bool hasXY = xIndex != -1 && yIndex != -1;
if ( wellNameIndex == -1 || unitNameIndex == -1 )
{
return std::unexpected( parseFailure );
}
if ( !hasMd && !hasTvd )
{
return std::unexpected( parseFailure + ". Neither MD or TVD is present." );
}
std::map<QString, std::vector<RigWellPathFormation>> formationsPerWell;
while ( !stream.atEnd() )
{
const QStringList columns = stream.readLine().split( delimiter );
if ( columns.size() != header.size() ) continue;
const QString wellName = columns[wellNameIndex];
const QString unitName = columns[unitNameIndex].trimmed();
if ( wellName.trimmed().isEmpty() && unitName.isEmpty() ) continue;
RigWellPathFormation formation;
formation.formationName = unitName;
if ( hasMd )
{
formation.mdTop = columns[mdTopIndex].toDouble();
formation.mdBase = columns[mdBaseIndex].toDouble();
}
if ( hasTvd )
{
const double sign = isTvdSS ? -1.0 : 1.0;
formation.tvdTop = sign * columns[tvdTopIndex].toDouble();
formation.tvdBase = sign * columns[tvdBaseIndex].toDouble();
}
if ( hasXY )
{
auto x = toOptionalDouble( columns, xIndex );
auto y = toOptionalDouble( columns, yIndex );
if ( x && y ) formation.topXY = cvf::Vec2d( *x, *y );
}
formationsPerWell[wellName].push_back( formation );
if ( zoneOrder && !unitName.isEmpty() && !zoneOrder->contains( unitName ) ) zoneOrder->push_back( unitName );
}
if ( formationsPerWell.empty() )
{
return std::unexpected( parseFailure );
}
WellFormations result;
for ( const auto& [wellName, formations] : formationsPerWell )
{
result.emplace( wellName, RigWellPathFormations( formations, filePath, wellName ) );
}
return result;
}