mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 15:10:22 -05:00
Migrate all assert macros in ApplicationLibCode to CAF_ASSERT and remove every use of cvfAssert.h. CVF_ASSERT is replaced one to one. CVF_TIGHT_ASSERT is also replaced by CAF_ASSERT, which is semantically exact: CVF_ENABLE_TIGHT_ASSERTS is 1 only under _DEBUG, and that is what CAF_ASSERT now does. The two CVF_FAIL_MSG sites become CAF_ASSERT( false && "message" ), preserving the message with the idiom already used elsewhere in the code base. Counts before and after: CVF_ASSERT 1044 to 0, CVF_TIGHT_ASSERT 66 to 0, CVF_FAIL_MSG 2 to 0, cvfAssert.h references 154 to 0. Include handling: files that included cvfAssert.h directly now include cafAssert.h instead, includes left dead by the migration are removed, and files that were relying on cvfAssert.h transitively get an explicit cafAssert.h. Files that reach cafAssert.h through another caf header are left unchanged; a missing include here is a compile error, not a silently disabled assert. ResultStatisticsCache links only LibCore and therefore had no path to cafAssert.h. Add the cafPdmCore directory as a private include path rather than linking the library, since cafAssert.h is header only. Note that this stops these asserts from firing in Release and RelWithDebInfo, where CVF_ASSERT was previously active.
573 lines
20 KiB
C++
573 lines
20 KiB
C++
/////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Copyright (C) 2016- 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 "RigLasFileExporter.h"
|
|
|
|
#include "RiaDefines.h"
|
|
#include "RiaGuiApplication.h"
|
|
|
|
#include "Well/RigWellLogCurveData.h"
|
|
#include "Well/RigWellPath.h"
|
|
|
|
#include "RimCase.h"
|
|
#include "RimWellLogCurve.h"
|
|
#include "RimWellLogExtractionCurve.h"
|
|
#include "RimWellPath.h"
|
|
|
|
#include "cafUtils.h"
|
|
|
|
#include "cafAssert.h"
|
|
|
|
#include "laswell.hpp"
|
|
|
|
#include <QDir>
|
|
#include <QMessageBox>
|
|
|
|
#include <cmath> // Needed for HUGE_VAL on Linux
|
|
|
|
namespace NRLib
|
|
{
|
|
class LasWell;
|
|
};
|
|
|
|
class SingleChannelData
|
|
{
|
|
public:
|
|
SingleChannelData( const std::string& channelName, const std::string& unit, const std::string& comment, const RigWellLogCurveData& curveData )
|
|
: m_channelName( channelName )
|
|
, m_unit( unit )
|
|
, m_comment( comment )
|
|
, m_curveData( curveData )
|
|
{
|
|
}
|
|
|
|
void appendDataToLasFile( NRLib::LasWell* lasFile, double absentValue ) const
|
|
{
|
|
CAF_ASSERT( lasFile );
|
|
|
|
if ( !m_curveData.propertyValues().empty() )
|
|
{
|
|
std::vector<double> wellLogValues = m_curveData.propertyValues( QString::fromStdString( m_unit ) );
|
|
for ( size_t vIdx = 0; vIdx < wellLogValues.size(); vIdx++ )
|
|
{
|
|
double value = wellLogValues[vIdx];
|
|
if ( value == HUGE_VAL || value == -HUGE_VAL || value != value )
|
|
{
|
|
wellLogValues[vIdx] = absentValue;
|
|
}
|
|
}
|
|
|
|
lasFile->AddLog( m_channelName, m_unit, m_comment, wellLogValues );
|
|
}
|
|
}
|
|
|
|
std::string channelName() const { return m_channelName; }
|
|
const RigWellLogCurveData& curveData() const { return m_curveData; }
|
|
|
|
private:
|
|
std::string m_channelName;
|
|
std::string m_unit;
|
|
std::string m_comment;
|
|
RigWellLogCurveData m_curveData;
|
|
};
|
|
|
|
class SingleLasFileMetaData
|
|
{
|
|
public:
|
|
SingleLasFileMetaData()
|
|
: m_rkbDiff( HUGE_VAL )
|
|
, m_exportTvdrkb( false )
|
|
, m_minimumCurveValue( HUGE_VAL )
|
|
{
|
|
}
|
|
|
|
void setWellName( const QString& wellName ) { m_wellName = wellName; }
|
|
|
|
QString wellName() { return m_wellName; }
|
|
|
|
void setCaseName( const QString& caseName ) { m_caseName = caseName; }
|
|
|
|
void setDate( const QString& date ) { m_date = date; }
|
|
|
|
void setRkbDiff( double rkbDiff ) { m_rkbDiff = rkbDiff; }
|
|
|
|
void enableTvdrkbExport() { m_exportTvdrkb = true; }
|
|
|
|
double rkbDiff() { return m_rkbDiff; }
|
|
|
|
void addLogData( const std::string& channelName, const std::string& unit, const std::string& comment, const RigWellLogCurveData& curveData )
|
|
{
|
|
m_logCurveData.push_back( SingleChannelData( channelName, unit, comment, curveData ) );
|
|
|
|
for ( double xValue : curveData.propertyValues() )
|
|
{
|
|
if ( xValue < m_minimumCurveValue )
|
|
{
|
|
m_minimumCurveValue = xValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string generateFilename() const
|
|
{
|
|
QString fileBasenameCandidate;
|
|
QString separator( "-" );
|
|
|
|
if ( !m_wellName.isEmpty() )
|
|
{
|
|
fileBasenameCandidate += m_wellName;
|
|
}
|
|
|
|
if ( !m_caseName.isEmpty() )
|
|
{
|
|
if ( !fileBasenameCandidate.isEmpty() ) fileBasenameCandidate += separator;
|
|
fileBasenameCandidate += m_caseName;
|
|
}
|
|
|
|
// Add property name if only one curve is exported
|
|
if ( m_logCurveData.size() == 1 )
|
|
{
|
|
if ( !fileBasenameCandidate.isEmpty() ) fileBasenameCandidate += separator;
|
|
fileBasenameCandidate += QString::fromStdString( m_logCurveData[0].channelName() );
|
|
}
|
|
|
|
if ( !m_date.isEmpty() )
|
|
{
|
|
if ( !fileBasenameCandidate.isEmpty() ) fileBasenameCandidate += separator;
|
|
fileBasenameCandidate += m_date;
|
|
}
|
|
|
|
QString cleanFileName = caf::Utils::makeValidFileBasename( fileBasenameCandidate );
|
|
|
|
cleanFileName += ".las";
|
|
|
|
return cleanFileName.toStdString();
|
|
}
|
|
|
|
void appendDataToLasFile( NRLib::LasWell* lasFile ) const
|
|
{
|
|
if ( m_logCurveData.empty() ) return;
|
|
|
|
lasFile->addWellInfo( "WELL", m_wellName.toStdString() );
|
|
|
|
QString wellLogDate = m_date;
|
|
wellLogDate.replace( ".", "_" );
|
|
wellLogDate.replace( " ", "_" );
|
|
|
|
lasFile->addWellInfo( "DATE", wellLogDate.toStdString() );
|
|
|
|
const RigWellLogCurveData& firstCurveData = curveDataForFirstCurve();
|
|
|
|
if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_METER )
|
|
{
|
|
lasFile->AddLog( "DEPTH", "M", "Depth in meters", firstCurveData.depths( RiaDefines::DepthType::MEASURED_DEPTH ) );
|
|
}
|
|
else if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_FEET )
|
|
{
|
|
lasFile->AddLog( "DEPTH", "FT", "Depth in feet", firstCurveData.depths( RiaDefines::DepthType::MEASURED_DEPTH ) );
|
|
}
|
|
else if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_NONE )
|
|
{
|
|
lasFile->AddLog( "DEPTH", "", "Depth in Connection number", firstCurveData.depths( RiaDefines::DepthType::MEASURED_DEPTH ) );
|
|
}
|
|
|
|
if ( !firstCurveData.depths( RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ).empty() )
|
|
{
|
|
lasFile->AddLog( "TVDMSL", "M", "True vertical depth in meters", firstCurveData.depths( RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) );
|
|
|
|
if ( m_exportTvdrkb && m_rkbDiff != -1.0 )
|
|
{
|
|
// Export True Vertical Depth Rotary Kelly Bushing - TVDRKB
|
|
std::vector<double> tvdrkbValues = firstCurveData.depths( RiaDefines::DepthType::TRUE_VERTICAL_DEPTH );
|
|
for ( auto& value : tvdrkbValues )
|
|
{
|
|
value += m_rkbDiff;
|
|
}
|
|
|
|
if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_METER )
|
|
{
|
|
lasFile->AddLog( "TVDRKB", "M", "True vertical depth (Rotary Kelly Bushing)", tvdrkbValues );
|
|
}
|
|
else if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_FEET )
|
|
{
|
|
lasFile->AddLog( "TVDRKB", "FT", "True vertical depth (Rotary Kelly Bushing)", tvdrkbValues );
|
|
}
|
|
else if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_NONE )
|
|
{
|
|
CAF_ASSERT( false );
|
|
lasFile->AddLog( "TVDRKB", "", "", tvdrkbValues );
|
|
}
|
|
}
|
|
}
|
|
|
|
double minDepth = 0.0;
|
|
double maxDepth = 0.0;
|
|
firstCurveData.calculateDepthRange( RiaDefines::DepthType::MEASURED_DEPTH, firstCurveData.depthUnit(), &minDepth, &maxDepth );
|
|
|
|
lasFile->setStartDepth( minDepth );
|
|
lasFile->setStopDepth( maxDepth );
|
|
|
|
if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_METER )
|
|
{
|
|
lasFile->setDepthUnit( "M" );
|
|
}
|
|
else if ( firstCurveData.depthUnit() == RiaDefines::DepthUnitType::UNIT_FEET )
|
|
{
|
|
lasFile->setDepthUnit( "FT" );
|
|
}
|
|
|
|
double absentValue = SingleLasFileMetaData::createAbsentValue( m_minimumCurveValue );
|
|
lasFile->SetMissing( absentValue );
|
|
|
|
for ( auto curveData : m_logCurveData )
|
|
{
|
|
curveData.appendDataToLasFile( lasFile, absentValue );
|
|
}
|
|
}
|
|
|
|
private:
|
|
const RigWellLogCurveData& curveDataForFirstCurve() const
|
|
{
|
|
CAF_ASSERT( !m_logCurveData.empty() );
|
|
|
|
return m_logCurveData[0].curveData();
|
|
}
|
|
|
|
static double createAbsentValue( double lowestDataValue )
|
|
{
|
|
double absentValue = -999.0;
|
|
|
|
while ( absentValue > lowestDataValue )
|
|
{
|
|
absentValue *= 10;
|
|
absentValue -= 9;
|
|
}
|
|
|
|
return absentValue - 0.25;
|
|
}
|
|
|
|
private:
|
|
QString m_wellName;
|
|
QString m_caseName;
|
|
QString m_date;
|
|
|
|
double m_rkbDiff;
|
|
bool m_exportTvdrkb;
|
|
|
|
std::vector<double> m_depthValues;
|
|
|
|
std::vector<SingleChannelData> m_logCurveData;
|
|
double m_minimumCurveValue;
|
|
};
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RigLasFileExporter::RigLasFileExporter( const std::vector<RimWellLogCurve*>& curves )
|
|
: m_curves( curves )
|
|
, m_isResampleActive( false )
|
|
, m_resamplingInterval( 1.0 )
|
|
{
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RigLasFileExporter::setResamplingInterval( double interval )
|
|
{
|
|
m_isResampleActive = true;
|
|
m_resamplingInterval = interval;
|
|
m_resampledCurveDatas.clear();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RigLasFileExporter::wellPathsAndRkbDiff( std::vector<QString>* wellNames, std::vector<double>* rkbDiffs )
|
|
{
|
|
std::vector<SingleLasFileMetaData> lasFileDescriptions = createLasFileDescriptions( m_curves, false );
|
|
|
|
std::set<QString> uniqueWellNames;
|
|
|
|
for ( auto metaData : lasFileDescriptions )
|
|
{
|
|
if ( metaData.rkbDiff() != std::numeric_limits<double>::infinity() )
|
|
{
|
|
QString wellName = metaData.wellName();
|
|
if ( uniqueWellNames.find( wellName ) == uniqueWellNames.end() )
|
|
{
|
|
uniqueWellNames.insert( wellName );
|
|
|
|
wellNames->push_back( wellName );
|
|
rkbDiffs->push_back( metaData.rkbDiff() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RigLasFileExporter::setRkbDiffs( const std::vector<QString>& wellNames, const std::vector<double>& rkbDiffs )
|
|
{
|
|
assert( wellNames.size() == rkbDiffs.size() );
|
|
|
|
std::vector<SingleLasFileMetaData> lasFileDescriptions = createLasFileDescriptions( m_curves, false );
|
|
|
|
for ( size_t i = 0; i < wellNames.size(); i++ )
|
|
{
|
|
for ( auto& metaData : lasFileDescriptions )
|
|
{
|
|
if ( metaData.wellName() == wellNames[i] )
|
|
{
|
|
m_userDefinedRkbOffsets.push_back( rkbDiffs[i] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<QString> RigLasFileExporter::writeToFolder( const QString& exportFolder,
|
|
const QString& filePrefix /*= ""*/,
|
|
bool capitalizeFileName /*= false*/,
|
|
bool alwaysOverwrite /*= false*/,
|
|
bool convertCurveUnits /*= false*/ )
|
|
{
|
|
std::vector<QString> writtenFiles;
|
|
|
|
std::vector<SingleLasFileMetaData> lasFileDescriptions = createLasFileDescriptions( m_curves, convertCurveUnits );
|
|
|
|
applyUserDefinedRkbOffsets( &lasFileDescriptions );
|
|
|
|
for ( auto lasFileDescr : lasFileDescriptions )
|
|
{
|
|
NRLib::LasWell lasFile;
|
|
lasFile.setVersionInfo( "2.0" );
|
|
|
|
lasFileDescr.appendDataToLasFile( &lasFile );
|
|
if ( m_isResampleActive )
|
|
{
|
|
lasFile.setDepthStep( m_resamplingInterval );
|
|
}
|
|
|
|
QDir dir( exportFolder );
|
|
QString fileName = filePrefix + QString::fromStdString( lasFileDescr.generateFilename() );
|
|
if ( capitalizeFileName )
|
|
{
|
|
fileName = fileName.toUpper();
|
|
}
|
|
QString fullPathName = dir.absoluteFilePath( fileName );
|
|
if ( caf::Utils::fileExists( fullPathName ) && !alwaysOverwrite )
|
|
{
|
|
QString txt = QString( "File %1 exists.\n\nDo you want to overwrite the file?" ).arg( fullPathName );
|
|
int ret = QMessageBox::question( RiaGuiApplication::widgetToUseAsParent(),
|
|
"LAS File Export",
|
|
txt,
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
QMessageBox::Yes );
|
|
|
|
if ( ret != QMessageBox::Yes ) continue;
|
|
}
|
|
|
|
std::vector<std::string> commentHeader;
|
|
lasFile.WriteToFile( fullPathName.toStdString(), commentHeader );
|
|
if ( QFileInfo::exists( fullPathName ) )
|
|
{
|
|
writtenFiles.push_back( fullPathName );
|
|
}
|
|
}
|
|
|
|
return writtenFiles;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<SingleLasFileMetaData> RigLasFileExporter::createLasFileDescriptions( const std::vector<RimWellLogCurve*>& curves,
|
|
bool convertCurveUnits )
|
|
{
|
|
std::vector<RimWellLogCurve*> eclipseCurves;
|
|
std::vector<RimWellLogCurve*> geoMechCurves;
|
|
std::vector<RimWellLogCurve*> externalLasCurves;
|
|
|
|
for ( auto curve : curves )
|
|
{
|
|
RimWellLogExtractionCurve* extractionCurve = dynamic_cast<RimWellLogExtractionCurve*>( curve );
|
|
if ( extractionCurve )
|
|
{
|
|
if ( extractionCurve->isEclipseCurve() )
|
|
{
|
|
eclipseCurves.push_back( extractionCurve );
|
|
}
|
|
else
|
|
{
|
|
geoMechCurves.push_back( extractionCurve );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
externalLasCurves.push_back( curve );
|
|
}
|
|
}
|
|
|
|
// External LAS files
|
|
|
|
std::vector<SingleLasFileMetaData> lasFileDescriptions;
|
|
|
|
appendLasFileDescriptions( externalLasCurves, &lasFileDescriptions, convertCurveUnits );
|
|
appendLasFileDescriptions( eclipseCurves, &lasFileDescriptions, convertCurveUnits );
|
|
appendLasFileDescriptions( geoMechCurves, &lasFileDescriptions, convertCurveUnits );
|
|
|
|
return lasFileDescriptions;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RigLasFileExporter::appendLasFileDescriptions( const std::vector<RimWellLogCurve*>& curves,
|
|
std::vector<SingleLasFileMetaData>* lasFileDescriptions,
|
|
bool convertCurveUnits )
|
|
{
|
|
CAF_ASSERT( lasFileDescriptions );
|
|
|
|
struct CurveCollectionDefinition
|
|
{
|
|
CurveCollectionDefinition( const QString& wellName, const QString& caseName, const QString& date )
|
|
: m_wellName( wellName )
|
|
, m_caseName( caseName )
|
|
, m_date( date )
|
|
{
|
|
}
|
|
|
|
bool isEqual( RimWellLogCurve* curve, const QString& caseName )
|
|
{
|
|
return m_wellName == curve->wellName() && m_caseName == caseName && m_date == curve->wellDate();
|
|
}
|
|
|
|
QString m_wellName;
|
|
QString m_caseName;
|
|
QString m_date;
|
|
};
|
|
|
|
std::vector<CurveCollectionDefinition> curveDefinitions;
|
|
for ( auto curve : curves )
|
|
{
|
|
QString caseName = caseNameFromCurve( curve );
|
|
|
|
bool found = false;
|
|
for ( auto curveDef : curveDefinitions )
|
|
{
|
|
if ( curveDef.isEqual( curve, caseName ) )
|
|
{
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if ( !found )
|
|
{
|
|
CurveCollectionDefinition curveDefCandidate( curve->wellName(), caseName, curve->wellDate() );
|
|
curveDefinitions.push_back( curveDefCandidate );
|
|
}
|
|
}
|
|
|
|
for ( auto curveDef : curveDefinitions )
|
|
{
|
|
SingleLasFileMetaData singleLasFileMeta;
|
|
singleLasFileMeta.setWellName( curveDef.m_wellName );
|
|
singleLasFileMeta.setCaseName( curveDef.m_caseName );
|
|
singleLasFileMeta.setDate( curveDef.m_date );
|
|
|
|
for ( auto curve : curves )
|
|
{
|
|
if ( curveDef.isEqual( curve, caseNameFromCurve( curve ) ) )
|
|
{
|
|
singleLasFileMeta.setRkbDiff( rkbDiff( curve ) );
|
|
|
|
const auto& curveData =
|
|
m_isResampleActive
|
|
? m_resampledCurveDatas.emplace_back( curve->curveData().calculateResampledCurveData( m_resamplingInterval ) )
|
|
: curve->curveData();
|
|
|
|
QString units = curveData.propertyValueUnit();
|
|
if ( convertCurveUnits || units == RiaWellLogUnitTools<double>::barX100UnitString() )
|
|
{
|
|
units = curve->wellLogChannelUnits();
|
|
}
|
|
|
|
singleLasFileMeta.addLogData( curve->wellLogChannelName().toStdString(), units.toStdString(), "", curveData );
|
|
}
|
|
}
|
|
|
|
lasFileDescriptions->push_back( singleLasFileMeta );
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RigLasFileExporter::caseNameFromCurve( RimWellLogCurve* curve )
|
|
{
|
|
QString caseName;
|
|
|
|
RimWellLogExtractionCurve* extractionCurve = dynamic_cast<RimWellLogExtractionCurve*>( curve );
|
|
if ( extractionCurve )
|
|
{
|
|
caseName = extractionCurve->caseName();
|
|
}
|
|
else
|
|
{
|
|
caseName = "unnamed";
|
|
}
|
|
|
|
return caseName;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
double RigLasFileExporter::rkbDiff( RimWellLogCurve* curve )
|
|
{
|
|
RimWellLogExtractionCurve* extractionCurve = dynamic_cast<RimWellLogExtractionCurve*>( curve );
|
|
if ( extractionCurve && extractionCurve->wellPath() && extractionCurve->wellPath()->wellPathGeometry() )
|
|
{
|
|
return extractionCurve->wellPath()->wellPathGeometry()->rkbDiff();
|
|
}
|
|
|
|
return HUGE_VAL;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RigLasFileExporter::applyUserDefinedRkbOffsets( std::vector<SingleLasFileMetaData>* lasFileDescriptions )
|
|
{
|
|
if ( m_userDefinedRkbOffsets.size() == lasFileDescriptions->size() )
|
|
{
|
|
for ( size_t i = 0; i < m_userDefinedRkbOffsets.size(); i++ )
|
|
{
|
|
if ( m_userDefinedRkbOffsets[i] != HUGE_VAL )
|
|
{
|
|
lasFileDescriptions->at( i ).setRkbDiff( m_userDefinedRkbOffsets[i] );
|
|
lasFileDescriptions->at( i ).enableTvdrkbExport();
|
|
}
|
|
}
|
|
}
|
|
}
|