diff --git a/ApplicationLibCode/Commands/OsduImportCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/OsduImportCommands/CMakeLists_files.cmake index af4c5c046d..25f302fe4c 100644 --- a/ApplicationLibCode/Commands/OsduImportCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/OsduImportCommands/CMakeLists_files.cmake @@ -5,6 +5,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellPathImport.h ${CMAKE_CURRENT_LIST_DIR}/RimWellsEntry.h ${CMAKE_CURRENT_LIST_DIR}/RiuWellImportWizard.h + ${CMAKE_CURRENT_LIST_DIR}/RiuWellLogImportWizard.h ${CMAKE_CURRENT_LIST_DIR}/RiaOsduOAuthHttpServerReplyHandler.h ${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.h ) @@ -16,6 +17,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellPathImport.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellsEntry.cpp ${CMAKE_CURRENT_LIST_DIR}/RiuWellImportWizard.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiuWellLogImportWizard.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaOsduOAuthHttpServerReplyHandler.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.cpp ) @@ -24,9 +26,13 @@ list(APPEND COMMAND_CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) list(APPEND COMMAND_CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) -list(APPEND COMMAND_QT_MOC_HEADERS ${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.h - ${CMAKE_CURRENT_LIST_DIR}/RiuWellImportWizard.h - ${CMAKE_CURRENT_LIST_DIR}/RiaOsduOAuthHttpServerReplyHandler.h +list( + APPEND + COMMAND_QT_MOC_HEADERS + ${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.h + ${CMAKE_CURRENT_LIST_DIR}/RiuWellImportWizard.h + ${CMAKE_CURRENT_LIST_DIR}/RiuWellLogImportWizard.h + ${CMAKE_CURRENT_LIST_DIR}/RiaOsduOAuthHttpServerReplyHandler.h ) source_group( diff --git a/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.cpp b/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.cpp index 54bfc48027..e8aa60997b 100644 --- a/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.cpp +++ b/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.cpp @@ -621,7 +621,24 @@ void RiaOsduConnector::parseWellLogs( QNetworkReply* reply, const QString& wellb QString id = resultObj["id"].toString(); QString kind = resultObj["kind"].toString(); - m_wellLogs[wellboreId].push_back( OsduWellLog{ id, kind, wellboreId } ); + QJsonArray curvesArray = resultObj["data"].toObject()["Curves"].toArray(); + QStringList curveIds; + RiaLogging::debug( QString( "Curves for '%1':" ).arg( id ) ); + for ( const QJsonValue& curve : curvesArray ) + { + QString curveId = curve["CurveID"].toString(); + QString curveDescription = curve["CurveDescription"].toString(); + double curveBaseDepth = curve["BaseDepth"].toDouble( 0.0 ); + double curveTopDepth = curve["TopDepth"].toDouble( 0.0 ); + + curveIds << curveId; + RiaLogging::debug( + QString( "%1: '%2' (%3 - %4)" ).arg( curveId ).arg( curveDescription ).arg( curveTopDepth ).arg( curveBaseDepth ) ); + } + + QString name = curveIds.join( ", " ); + + m_wellLogs[wellboreId].push_back( OsduWellLog{ id, kind, wellboreId, name } ); } } @@ -691,6 +708,20 @@ std::vector RiaOsduConnector::wells() const return m_wells; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RiaOsduConnector::wellLogs( const QString& wellboreId ) const +{ + QMutexLocker lock( &m_mutex ); + + auto it = m_wellLogs.find( wellboreId ); + if ( it != m_wellLogs.end() ) + return it->second; + else + return {}; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.h b/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.h index 32a0f44c1d..8198e24b6f 100644 --- a/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.h +++ b/ApplicationLibCode/Commands/OsduImportCommands/RiaOsduConnector.h @@ -42,6 +42,7 @@ struct OsduWellLog QString id; QString kind; QString wellboreId; + QString name; }; //================================================================================================== diff --git a/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.cpp b/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.cpp new file mode 100644 index 0000000000..dd95c6ba72 --- /dev/null +++ b/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.cpp @@ -0,0 +1,290 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2024- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RiuWellLogImportWizard.h" + +#include "RiaFeatureCommandContext.h" +#include "RiaOsduConnector.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiuWellLogImportWizard::RiuWellLogImportWizard( RiaOsduConnector* osduConnector, const QString& wellboreId, QWidget* parent /*= 0*/ ) + : QWizard( parent ) +{ + m_osduConnector = osduConnector; + m_wellboreId = wellboreId; + + addPage( new WellLogAuthenticationPage( m_osduConnector, this ) ); + addPage( new WellLogSelectionPage( m_osduConnector, this ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiuWellLogImportWizard::~RiuWellLogImportWizard() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuWellLogImportWizard::downloadWellLogs( const QString& wellboreId ) +{ + m_osduConnector->requestWellLogsByWellboreId( wellboreId ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuWellLogImportWizard::setSelectedWellLogs( const std::vector& wellboreIds ) +{ + m_selectedWellLogs = wellboreIds; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RiuWellLogImportWizard::selectedWellLogs() const +{ + return m_selectedWellLogs; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiuWellLogImportWizard::wellboreId() const +{ + return m_wellboreId; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RiuWellLogImportWizard::importedWellLogs() const +{ + return m_wellLogInfos; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuWellLogImportWizard::addWellLogInfo( RiuWellLogImportWizard::WellLogInfo wellLogInfo ) +{ + m_wellLogInfos.push_back( wellLogInfo ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +WellLogAuthenticationPage::WellLogAuthenticationPage( RiaOsduConnector* osduConnector, QWidget* parent /*= 0*/ ) + : QWizardPage( parent ) + , m_osduConnector( osduConnector ) + , m_accessOk( false ) +{ + setTitle( "OSDU - Login" ); + + QVBoxLayout* layout = new QVBoxLayout; + + m_connectionLabel = new QLabel( "Checking OSDU connection. You might need to login." ); + layout->addWidget( m_connectionLabel ); + + QFormLayout* formLayout = new QFormLayout; + layout->addLayout( formLayout ); + + QLineEdit* serverLineEdit = new QLineEdit( osduConnector->server(), this ); + serverLineEdit->setReadOnly( true ); + QLineEdit* partitionLineEdit = new QLineEdit( osduConnector->dataPartition(), this ); + partitionLineEdit->setReadOnly( true ); + + formLayout->addRow( "Server:", serverLineEdit ); + formLayout->addRow( "Data Partition:", partitionLineEdit ); + + setLayout( layout ); + + connect( osduConnector, SIGNAL( tokenReady( const QString& ) ), this, SLOT( accessOk() ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void WellLogAuthenticationPage::initializePage() +{ + m_osduConnector->requestToken(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool WellLogAuthenticationPage::isComplete() const +{ + return m_accessOk; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void WellLogAuthenticationPage::accessOk() +{ + m_connectionLabel->setText( "Connection to OSDU: OK." ); + m_accessOk = true; + emit( completeChanged() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +WellLogSelectionPage::WellLogSelectionPage( RiaOsduConnector* osduConnector, QWidget* parent /*= 0*/ ) +{ + QVBoxLayout* layout = new QVBoxLayout; + setLayout( layout ); + + QLabel* label = new QLabel( "Select well logs" ); + layout->addWidget( label ); + + QHBoxLayout* filterLayout = new QHBoxLayout; + filterLayout->addWidget( new QLabel( "Filter:", this ) ); + QLineEdit* filterLineEdit = new QLineEdit( this ); + filterLayout->addWidget( filterLineEdit ); + + layout->addLayout( filterLayout ); + + m_tableView = new QTableView( this ); + m_tableView->setSelectionBehavior( QAbstractItemView::SelectRows ); + m_tableView->setSelectionMode( QAbstractItemView::MultiSelection ); + m_tableView->setSortingEnabled( true ); + int nameColumn = 2; + m_tableView->sortByColumn( nameColumn, Qt::AscendingOrder ); + + QHeaderView* header = m_tableView->horizontalHeader(); + header->setSectionResizeMode( QHeaderView::Interactive ); + header->setStretchLastSection( true ); + + m_osduWellLogsModel = new OsduWellLogTableModel; + layout->addWidget( m_tableView ); + layout->setStretchFactor( m_tableView, 10 ); + + m_proxyModel = new QSortFilterProxyModel( this ); + m_proxyModel->setSourceModel( m_osduWellLogsModel ); + m_proxyModel->setFilterKeyColumn( nameColumn ); + m_proxyModel->setFilterCaseSensitivity( Qt::CaseInsensitive ); + + m_tableView->setModel( m_proxyModel ); + m_tableView->setSortingEnabled( true ); + + QObject::connect( filterLineEdit, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterWildcard ); + + m_osduConnector = osduConnector; + connect( m_osduConnector, SIGNAL( wellLogsFinished( const QString& ) ), SLOT( wellLogsFinished( const QString& ) ) ); + + connect( m_tableView->selectionModel(), + SIGNAL( selectionChanged( const QItemSelection&, const QItemSelection& ) ), + SLOT( selectWellLogs( const QItemSelection&, const QItemSelection& ) ) ); + + connect( m_tableView->selectionModel(), + SIGNAL( selectionChanged( const QItemSelection&, const QItemSelection& ) ), + SIGNAL( completeChanged() ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void WellLogSelectionPage::initializePage() +{ + RiuWellLogImportWizard* wiz = dynamic_cast( wizard() ); + if ( !wiz ) return; + + QString wellboreId = wiz->wellboreId(); + wiz->downloadWellLogs( wellboreId ); + + setButtonText( QWizard::NextButton, "Next" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +WellLogSelectionPage::~WellLogSelectionPage() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void WellLogSelectionPage::wellLogsFinished( const QString& wellboreId ) +{ + std::vector wellLogs = m_osduConnector->wellLogs( wellboreId ); + m_osduWellLogsModel->setOsduWellLogs( wellLogs ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool WellLogSelectionPage::isComplete() const +{ + QItemSelectionModel* select = m_tableView->selectionModel(); + return !select->selectedRows().empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void WellLogSelectionPage::selectWellLogs( const QItemSelection& newSelection, const QItemSelection& oldSelection ) +{ + if ( !newSelection.indexes().empty() ) + { + RiuWellLogImportWizard* wiz = dynamic_cast( wizard() ); + + std::vector wellLogs = m_osduConnector->wellLogs( wiz->wellboreId() ); + + auto findWellLogById = []( const std::vector& wellLogs, const QString& wellLogId ) -> std::optional + { + auto it = std::find_if( wellLogs.begin(), wellLogs.end(), [wellLogId]( const OsduWellLog& w ) { return w.id == wellLogId; } ); + if ( it != wellLogs.end() ) + return std::optional( *it ); + else + return {}; + }; + + QModelIndexList selection = m_tableView->selectionModel()->selectedRows(); + for ( QModelIndex index : selection ) + { + int idColumn = 0; + + if ( index.column() == idColumn ) + { + QString wellLogId = m_proxyModel->data( index.siblingAtColumn( idColumn ) ).toString(); + std::optional wellLog = findWellLogById( wellLogs, wellLogId ); + if ( wellLog.has_value() ) + { + wiz->addWellLogInfo( { .name = wellLog->name, .wellLog = wellLogId } ); + } + } + } + } +} diff --git a/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.h b/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.h new file mode 100644 index 0000000000..dae2b6f5d9 --- /dev/null +++ b/ApplicationLibCode/Commands/OsduImportCommands/RiuWellLogImportWizard.h @@ -0,0 +1,226 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2024 Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "RiaOsduConnector.h" + +#include + +class QFile; +class QLabel; +class QTextEdit; +class QTableView; + +class RimWellPathImport; + +class OsduWellLogTableModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit OsduWellLogTableModel( QObject* parent = nullptr ) + : QAbstractTableModel( parent ) + { + } + + int rowCount( const QModelIndex& parent = QModelIndex() ) const override + { + Q_UNUSED( parent ); + return static_cast( m_osduWellLogs.size() ); + } + + int columnCount( const QModelIndex& parent = QModelIndex() ) const override + { + Q_UNUSED( parent ); + // Assuming you have three fields: id, kind, and name + return 3; + } + + QVariant data( const QModelIndex& index, int role = Qt::DisplayRole ) const override + { + if ( !index.isValid() ) return QVariant(); + + if ( index.row() >= static_cast( m_osduWellLogs.size() ) || index.row() < 0 ) return QVariant(); + + if ( role == Qt::DisplayRole ) + { + const OsduWellLog& field = m_osduWellLogs.at( index.row() ); + switch ( index.column() ) + { + case 0: + return field.id; + case 1: + return field.kind; + case 2: + return field.name; + default: + return QVariant(); + } + } + + return QVariant(); + } + + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override + { + if ( role != Qt::DisplayRole ) return QVariant(); + + if ( orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "ID" ); + case 1: + return tr( "Kind" ); + case 2: + return tr( "Name" ); + default: + return QVariant(); + } + } + return QVariant(); + } + + void setOsduWellLogs( const std::vector& osduWellLogs ) + { + beginResetModel(); + m_osduWellLogs.clear(); + for ( auto v : osduWellLogs ) + m_osduWellLogs.push_back( v ); + + endResetModel(); + } + + void sort( int column, Qt::SortOrder order = Qt::AscendingOrder ) override + { + std::sort( m_osduWellLogs.begin(), + m_osduWellLogs.end(), + [column, order]( const OsduWellLog& a, const OsduWellLog& b ) + { + switch ( column ) + { + case 0: + return ( order == Qt::AscendingOrder ) ? a.id < b.id : a.id > b.id; + case 1: + return ( order == Qt::AscendingOrder ) ? a.kind < b.kind : a.kind > b.kind; + case 2: + return ( order == Qt::AscendingOrder ) ? a.name < b.name : a.name > b.name; + default: + return false; + } + } ); + emit dataChanged( index( 0, 0 ), index( rowCount() - 1, columnCount() - 1 ) ); + emit layoutChanged(); + } + +private: + std::vector m_osduWellLogs; +}; + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +class WellLogAuthenticationPage : public QWizardPage +{ + Q_OBJECT + +public: + WellLogAuthenticationPage( RiaOsduConnector* osduConnector, QWidget* parent = nullptr ); + + void initializePage() override; + bool isComplete() const override; + +private slots: + void accessOk(); + +private: + RiaOsduConnector* m_osduConnector; + QLabel* m_connectionLabel; + bool m_accessOk; +}; + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +class WellLogSelectionPage : public QWizardPage +{ + Q_OBJECT + +public: + WellLogSelectionPage( RiaOsduConnector* m_osduConnector, QWidget* parent = nullptr ); + ~WellLogSelectionPage() override; + + void initializePage() override; + bool isComplete() const override; + +private slots: + void wellLogsFinished( const QString& wellboreId ); + void selectWellLogs( const QItemSelection& newSelection, const QItemSelection& oldSelection ); + +private: + RiaOsduConnector* m_osduConnector; + + QTableView* m_tableView; + OsduWellLogTableModel* m_osduWellLogsModel; + QSortFilterProxyModel* m_proxyModel; +}; + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +class RiuWellLogImportWizard : public QWizard +{ + Q_OBJECT + +public: + struct WellLogInfo + { + QString name; + QString wellLog; + }; + + RiuWellLogImportWizard( RiaOsduConnector* osduConnector, const QString& wellboreId, QWidget* parent = nullptr ); + ~RiuWellLogImportWizard() override; + + void setSelectedWellLogs( const std::vector& wellLogIds ); + std::vector selectedWellLogs() const; + + void addWellLogInfo( RiuWellLogImportWizard::WellLogInfo wellLogInfo ); + std::vector importedWellLogs() const; + + QString wellboreId() const; + +public slots: + void downloadWellLogs( const QString& wellboreId ); + +private: + RiaOsduConnector* m_osduConnector; + std::vector m_selectedWellLogs; + + QString m_wellboreId; + std::vector m_wellLogInfos; +}; diff --git a/ApplicationLibCode/Commands/RicImportWellLogOsduFeature.cpp b/ApplicationLibCode/Commands/RicImportWellLogOsduFeature.cpp index 9f34260a49..b1df7f3785 100644 --- a/ApplicationLibCode/Commands/RicImportWellLogOsduFeature.cpp +++ b/ApplicationLibCode/Commands/RicImportWellLogOsduFeature.cpp @@ -30,6 +30,7 @@ #include "RiuMainWindow.h" #include "OsduImportCommands/RiaOsduConnector.h" +#include "OsduImportCommands/RiuWellLogImportWizard.h" #include "RiaLogging.h" #include "RiaPreferences.h" @@ -61,25 +62,31 @@ void RicImportWellLogOsduFeature::onActionTriggered( bool isChecked ) if ( !oilField->wellPathCollection ) oilField->wellPathCollection = std::make_unique(); - auto osduConnector = app->makeOsduConnector(); - std::vector wellLogs = osduConnector->requestWellLogsByWellboreIdBlocking( wellPath->wellboreId() ); + auto osduConnector = app->makeOsduConnector(); - for ( OsduWellLog wellLog : wellLogs ) + RiuWellLogImportWizard wellLogImportWizard( osduConnector, wellPath->wellboreId(), RiuMainWindow::instance() ); + + if ( QDialog::Accepted == wellLogImportWizard.exec() ) { - auto [wellLogData, errorMessage] = RimWellPathCollection::loadWellLogFromOsdu( osduConnector, wellLog.id ); - if ( wellLogData.notNull() ) - { - RimOsduWellLog* osduWellLog = new RimOsduWellLog; - osduWellLog->setName( wellLog.id ); - osduWellLog->setWellLogId( wellLog.id ); - oilField->wellPathCollection->addWellLog( osduWellLog, wellPath ); + std::vector wellLogs = wellLogImportWizard.importedWellLogs(); - osduWellLog->setWellLogData( wellLogData.p() ); - osduWellLog->updateConnectedEditors(); - } - else + for ( RiuWellLogImportWizard::WellLogInfo wellLog : wellLogs ) { - RiaLogging::error( "Importing OSDU well log failed: " + errorMessage ); + auto [wellLogData, errorMessage] = RimWellPathCollection::loadWellLogFromOsdu( osduConnector, wellLog.wellLog ); + if ( wellLogData.notNull() ) + { + RimOsduWellLog* osduWellLog = new RimOsduWellLog; + osduWellLog->setName( wellLog.name ); + osduWellLog->setWellLogId( wellLog.wellLog ); + oilField->wellPathCollection->addWellLog( osduWellLog, wellPath ); + + osduWellLog->setWellLogData( wellLogData.p() ); + osduWellLog->updateConnectedEditors(); + } + else + { + RiaLogging::error( "Importing OSDU well log failed: " + errorMessage ); + } } } }