mirror of
https://github.com/OPM/ResInsight.git
synced 2025-02-25 18:55:39 -06:00
Add features to create polygons from visible contour map geometry
This commit is contained in:
@@ -99,6 +99,10 @@ set(SOURCE_GROUP_HEADER_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewWellTargetCandidatesGeneratorFeature.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewStatisticsContourMapFeature.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewStatisticsContourMapViewFeature.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonFeature.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonAdvancedFeature.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonTools.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicPolygonFromImageDialog.h
|
||||
)
|
||||
|
||||
set(SOURCE_GROUP_SOURCE_FILES
|
||||
@@ -202,6 +206,10 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewWellTargetCandidatesGeneratorFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewStatisticsContourMapFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewStatisticsContourMapViewFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonAdvancedFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreateContourMapPolygonTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicPolygonFromImageDialog.cpp
|
||||
)
|
||||
|
||||
if(RESINSIGHT_USE_QT_CHARTS)
|
||||
|
@@ -0,0 +1,60 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025- 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 "RicCreateContourMapPolygonAdvancedFeature.h"
|
||||
|
||||
#include "RicCreateContourMapPolygonTools.h"
|
||||
#include "RicPolygonFromImageDialog.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
CAF_CMD_SOURCE_INIT( RicCreateContourMapPolygonAdvancedFeature, "RicCreateContourMapPolygonAdvancedFeature" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonAdvancedFeature::onActionTriggered( bool isChecked )
|
||||
{
|
||||
auto rigContourMapProjection = RicCreateContourMapPolygonTools::findCurrentContourMapProjection();
|
||||
if ( !rigContourMapProjection ) return;
|
||||
|
||||
auto sourceImage = RicCreateContourMapPolygonTools::convertToBinaryImage( rigContourMapProjection );
|
||||
|
||||
ImageProcessingDialog dlg;
|
||||
dlg.setSourceImageData( sourceImage );
|
||||
dlg.show();
|
||||
dlg.updateAndShowImages();
|
||||
|
||||
if ( dlg.exec() == QDialog::Rejected ) return;
|
||||
|
||||
auto processedImage = dlg.processedImageData();
|
||||
if ( processedImage.empty() ) return;
|
||||
|
||||
RicCreateContourMapPolygonTools::createAndAddBoundaryPolygonFromImage( processedImage, rigContourMapProjection );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonAdvancedFeature::setupActionLook( QAction* actionToSetup )
|
||||
{
|
||||
actionToSetup->setIcon( QIcon( ":/PolylinesFromFile16x16.png" ) );
|
||||
actionToSetup->setText( "Create Polygon From Contour Map (Developers Only)" );
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025- 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 "cafCmdFeature.h"
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
class RicCreateContourMapPolygonAdvancedFeature : public caf::CmdFeature
|
||||
{
|
||||
CAF_CMD_HEADER_INIT;
|
||||
|
||||
protected:
|
||||
void onActionTriggered( bool isChecked ) override;
|
||||
void setupActionLook( QAction* actionToSetup ) override;
|
||||
};
|
@@ -0,0 +1,58 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025- 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 "RicCreateContourMapPolygonFeature.h"
|
||||
|
||||
#include "RicCreateContourMapPolygonTools.h"
|
||||
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
CAF_CMD_SOURCE_INIT( RicCreateContourMapPolygonFeature, "RicCreateContourMapPolygonFeature" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonFeature::onActionTriggered( bool isChecked )
|
||||
{
|
||||
auto rigContourMapProjection = RicCreateContourMapPolygonTools::findCurrentContourMapProjection();
|
||||
if ( !rigContourMapProjection ) return;
|
||||
|
||||
auto sourceImage = RicCreateContourMapPolygonTools::convertToBinaryImage( rigContourMapProjection );
|
||||
|
||||
const int kernelSize = 3;
|
||||
auto floodFilled = RigPolygonTools::fillInterior( sourceImage );
|
||||
auto eroded = RigPolygonTools::erode( floodFilled, kernelSize );
|
||||
auto dilated = RigPolygonTools::dilate( eroded, kernelSize );
|
||||
|
||||
if ( dilated.empty() ) return;
|
||||
|
||||
RicCreateContourMapPolygonTools::createAndAddBoundaryPolygonFromImage( dilated, rigContourMapProjection );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonFeature::setupActionLook( QAction* actionToSetup )
|
||||
{
|
||||
actionToSetup->setIcon( QIcon( ":/PolylinesFromFile16x16.png" ) );
|
||||
actionToSetup->setText( "Create Polygon From Contour Map" );
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025- 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 "cafCmdFeature.h"
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
class RicCreateContourMapPolygonFeature : public caf::CmdFeature
|
||||
{
|
||||
CAF_CMD_HEADER_INIT;
|
||||
|
||||
protected:
|
||||
void onActionTriggered( bool isChecked ) override;
|
||||
void setupActionLook( QAction* actionToSetup ) override;
|
||||
};
|
250
ApplicationLibCode/Commands/RicCreateContourMapPolygonTools.cpp
Normal file
250
ApplicationLibCode/Commands/RicCreateContourMapPolygonTools.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RicCreateContourMapPolygonTools.h"
|
||||
|
||||
#include "RicExportContourMapToTextFeature.h"
|
||||
|
||||
#include "RigContourMapProjection.h"
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include "Polygons/RimPolygon.h"
|
||||
#include "Polygons/RimPolygonCollection.h"
|
||||
#include "RimContourMapProjection.h"
|
||||
#include "RimEclipseContourMapView.h"
|
||||
#include "RimGeoMechContourMapView.h"
|
||||
#include "RimTools.h"
|
||||
|
||||
#include "cvfBase.h"
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QImage RicCreateContourMapPolygonTools::convertBinaryToImage( const std::vector<std::vector<int>>& data, QColor color, int transparency )
|
||||
{
|
||||
if ( data.empty() || data[0].empty() )
|
||||
{
|
||||
qWarning( "Data is empty. Cannot export an image." );
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get dimensions
|
||||
int height = static_cast<int>( data[0].size() );
|
||||
int width = static_cast<int>( data.size() );
|
||||
|
||||
// Create a QImage
|
||||
QImage image( width, height, QImage::Format_ARGB32 );
|
||||
|
||||
// Fill QImage with data
|
||||
for ( int y = 0; y < height; ++y )
|
||||
{
|
||||
for ( int x = 0; x < width; ++x )
|
||||
{
|
||||
int value = std::clamp( data[x][y], 0, 255 );
|
||||
if ( value > 0 )
|
||||
image.setPixel( x, height - y - 1, qRgba( color.red(), color.green(), color.blue(), transparency ) ); // Grayscale
|
||||
else
|
||||
{
|
||||
image.setPixel( x, height - y - 1, qRgba( 0, 0, 0, transparency ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QImage RicCreateContourMapPolygonTools::convertBinaryToGrayscaleImage( const std::vector<std::vector<int>>& data, int colorValue )
|
||||
{
|
||||
if ( data.empty() || data[0].empty() )
|
||||
{
|
||||
qWarning( "Data is empty. Cannot export an image." );
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get dimensions
|
||||
int height = static_cast<int>( data[0].size() );
|
||||
int width = static_cast<int>( data.size() );
|
||||
|
||||
// Create a QImage
|
||||
QImage image( width, height, QImage::Format_Grayscale8 );
|
||||
|
||||
// Fill QImage with data
|
||||
for ( int y = 0; y < height; ++y )
|
||||
{
|
||||
for ( int x = 0; x < width; ++x )
|
||||
{
|
||||
int value = std::clamp( data[x][y] * colorValue, 0, 255 ); // Ensure value is in [0, 255]
|
||||
image.setPixel( x, height - y - 1, qRgb( value, value, value ) ); // Grayscale
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonTools::exportVectorAsImage( const std::vector<std::vector<int>>& data, int transparency, const QString& filename )
|
||||
{
|
||||
if ( data.empty() || data[0].empty() )
|
||||
{
|
||||
qWarning( "Data is empty. Cannot export an image." );
|
||||
return;
|
||||
}
|
||||
|
||||
auto image = convertBinaryToImage( data, QColorConstants::Green, transparency );
|
||||
|
||||
if ( !image.save( filename, "PNG" ) )
|
||||
{
|
||||
qWarning( "Failed to save image as PNG." );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicCreateContourMapPolygonTools::exportVectorAsGrayscaleImage( const std::vector<std::vector<int>>& data, const QString& filename )
|
||||
{
|
||||
if ( data.empty() || data[0].empty() )
|
||||
{
|
||||
qWarning( "Data is empty. Cannot export an image." );
|
||||
return;
|
||||
}
|
||||
|
||||
auto image = convertBinaryToGrayscaleImage( data, 255 );
|
||||
|
||||
// Save the QImage as a PNG file
|
||||
if ( !image.save( filename, "PNG" ) )
|
||||
{
|
||||
qWarning( "Failed to save image as PNG." );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::vector<int>> RicCreateContourMapPolygonTools::convertImageToBinary( QImage image )
|
||||
{
|
||||
std::vector<std::vector<int>> binaryImage( image.width(), std::vector<int>( image.height(), 0 ) );
|
||||
for ( int i = 0; i < image.width(); ++i )
|
||||
{
|
||||
for ( int j = 0; j < image.height(); ++j )
|
||||
{
|
||||
auto pixelColor = image.pixel( i, j );
|
||||
auto gray = qGray( pixelColor );
|
||||
|
||||
binaryImage[i][image.height() - j - 1] = gray > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return binaryImage;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::vector<int>> RicCreateContourMapPolygonTools::convertToBinaryImage( const RigContourMapProjection* rigContourMapProjection )
|
||||
{
|
||||
if ( !rigContourMapProjection ) return {};
|
||||
|
||||
auto vertexSizeIJ = rigContourMapProjection->numberOfVerticesIJ();
|
||||
|
||||
std::vector<std::vector<int>> image( vertexSizeIJ.x(), std::vector<int>( vertexSizeIJ.y(), 0 ) );
|
||||
|
||||
for ( cvf::uint i = 0; i < vertexSizeIJ.x(); i++ )
|
||||
{
|
||||
for ( cvf::uint j = 0; j < vertexSizeIJ.y(); j++ )
|
||||
{
|
||||
double valueAtVertex = rigContourMapProjection->filteredValueAtVertex( i, j );
|
||||
|
||||
if ( !std::isinf( valueAtVertex ) )
|
||||
{
|
||||
image[i][j] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
image[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimPolygon* RicCreateContourMapPolygonTools::createAndAddBoundaryPolygonFromImage( std::vector<std::vector<int>> image,
|
||||
const RigContourMapProjection* contourMapProjection )
|
||||
{
|
||||
if ( !contourMapProjection ) return nullptr;
|
||||
if ( image.empty() ) return nullptr;
|
||||
|
||||
std::vector<cvf::Vec3d> polygonDomainCoords;
|
||||
auto xVertexPositions = contourMapProjection->xVertexPositions();
|
||||
auto yVertexPositions = contourMapProjection->yVertexPositions();
|
||||
auto origin3d = contourMapProjection->origin3d();
|
||||
auto depth = contourMapProjection->topDepthBoundingBox();
|
||||
|
||||
auto boundaryPoints = RigPolygonTools::boundary( image );
|
||||
for ( auto [i, j] : boundaryPoints )
|
||||
{
|
||||
double xDomain = xVertexPositions.at( i ) + origin3d.x();
|
||||
double yDomain = yVertexPositions.at( j ) + origin3d.y();
|
||||
|
||||
polygonDomainCoords.emplace_back( cvf::Vec3d( xDomain, yDomain, depth ) );
|
||||
}
|
||||
|
||||
// Epsilon used to simplify polygon. Useful range typical value in [5..50]
|
||||
const double defaultEpsilon = 40.0;
|
||||
RigPolygonTools::simplifyPolygon( polygonDomainCoords, defaultEpsilon );
|
||||
|
||||
if ( polygonDomainCoords.size() >= 3 )
|
||||
{
|
||||
auto polygonCollection = RimTools::polygonCollection();
|
||||
|
||||
auto newPolygon = polygonCollection->appendUserDefinedPolygon();
|
||||
|
||||
newPolygon->setPointsInDomainCoords( polygonDomainCoords );
|
||||
newPolygon->coordinatesChanged.send();
|
||||
|
||||
polygonCollection->uiCapability()->updateAllRequiredEditors();
|
||||
|
||||
return newPolygon;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
const RigContourMapProjection* RicCreateContourMapPolygonTools::findCurrentContourMapProjection()
|
||||
{
|
||||
RimContourMapProjection* contourMapProjection = nullptr;
|
||||
|
||||
auto [existingEclipseContourMap, existingGeoMechContourMap] = RicExportContourMapToTextFeature::findContourMapView();
|
||||
if ( existingEclipseContourMap ) contourMapProjection = existingEclipseContourMap->contourMapProjection();
|
||||
if ( existingGeoMechContourMap ) contourMapProjection = existingGeoMechContourMap->contourMapProjection();
|
||||
|
||||
if ( !contourMapProjection ) return nullptr;
|
||||
|
||||
return contourMapProjection->mapProjection();
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class RigContourMapProjection;
|
||||
class RimPolygon;
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
namespace RicCreateContourMapPolygonTools
|
||||
{
|
||||
QImage convertBinaryToImage( const std::vector<std::vector<int>>& data, QColor color, int transparency );
|
||||
QImage convertBinaryToGrayscaleImage( const std::vector<std::vector<int>>& data, int colorValue );
|
||||
|
||||
void exportVectorAsImage( const std::vector<std::vector<int>>& data, int transparency, const QString& filename );
|
||||
void exportVectorAsGrayscaleImage( const std::vector<std::vector<int>>& data, const QString& filename );
|
||||
|
||||
std::vector<std::vector<int>> convertImageToBinary( QImage image );
|
||||
std::vector<std::vector<int>> convertToBinaryImage( const RigContourMapProjection* contourMapProjection );
|
||||
|
||||
RimPolygon* createAndAddBoundaryPolygonFromImage( std::vector<std::vector<int>> image, const RigContourMapProjection* contourMapProjection );
|
||||
|
||||
const RigContourMapProjection* findCurrentContourMapProjection();
|
||||
|
||||
}; // namespace RicCreateContourMapPolygonTools
|
@@ -27,9 +27,7 @@
|
||||
#include "RigContourMapProjection.h"
|
||||
|
||||
#include "RimContourMapProjection.h"
|
||||
#include "RimEclipseContourMapProjection.h"
|
||||
#include "RimEclipseContourMapView.h"
|
||||
#include "RimGeoMechContourMapProjection.h"
|
||||
#include "RimGeoMechContourMapView.h"
|
||||
#include "RimProject.h"
|
||||
#include "RimViewWindow.h"
|
||||
|
@@ -41,6 +41,8 @@ public:
|
||||
RicExportContourMapToTextFeature();
|
||||
caf::PdmScriptResponse execute() override;
|
||||
|
||||
static std::pair<RimEclipseContourMapView*, RimGeoMechContourMapView*> findContourMapView();
|
||||
|
||||
protected:
|
||||
bool isCommandEnabled() const override;
|
||||
void onActionTriggered( bool isChecked ) override;
|
||||
@@ -57,9 +59,6 @@ protected:
|
||||
const QString& undefinedValueLabel,
|
||||
bool excludeUndefinedValues );
|
||||
|
||||
private:
|
||||
static std::pair<RimEclipseContourMapView*, RimGeoMechContourMapView*> findContourMapView();
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_exportFileName;
|
||||
caf::PdmField<bool> m_exportLocalCoordinates;
|
||||
|
287
ApplicationLibCode/Commands/RicPolygonFromImageDialog.cpp
Normal file
287
ApplicationLibCode/Commands/RicPolygonFromImageDialog.cpp
Normal file
@@ -0,0 +1,287 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RicPolygonFromImageDialog.h"
|
||||
|
||||
#include "RicCreateContourMapPolygonTools.h"
|
||||
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
ImageProcessingDialog::ImageProcessingDialog( QWidget* parent /*= nullptr */ )
|
||||
: QDialog( parent )
|
||||
{
|
||||
setWindowTitle( "Image Morphology Tool" );
|
||||
|
||||
// Layout and widgets
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout( this );
|
||||
|
||||
QHBoxLayout* centerLayout = new QHBoxLayout( this );
|
||||
mainLayout->addLayout( centerLayout );
|
||||
|
||||
QVBoxLayout* settingsLayout = new QVBoxLayout( this );
|
||||
centerLayout->addLayout( settingsLayout );
|
||||
|
||||
QLabel* kernelLabel = new QLabel( "Kernel Size:", this );
|
||||
settingsLayout->addWidget( kernelLabel );
|
||||
|
||||
kernelSpinBox = new QSpinBox( this );
|
||||
kernelSpinBox->setRange( 1, 31 );
|
||||
kernelSpinBox->setValue( 3 );
|
||||
settingsLayout->addWidget( kernelSpinBox );
|
||||
|
||||
QLabel* transparencyLabel = new QLabel( "Transparency (0-100):", this );
|
||||
settingsLayout->addWidget( transparencyLabel );
|
||||
|
||||
transparencySlider = new QSlider( Qt::Horizontal, this );
|
||||
transparencySlider->setRange( 0, 100 );
|
||||
transparencySlider->setValue( 90 );
|
||||
settingsLayout->addWidget( transparencySlider );
|
||||
|
||||
showInput = new QCheckBox( "Show Input Image", this );
|
||||
showInput->setChecked( true );
|
||||
settingsLayout->addWidget( showInput );
|
||||
|
||||
showDilated = new QCheckBox( "Show Dilated Image", this );
|
||||
showDilated->setChecked( false );
|
||||
settingsLayout->addWidget( showDilated );
|
||||
|
||||
showEroded = new QCheckBox( "Show Eroded Image", this );
|
||||
showEroded->setChecked( false );
|
||||
settingsLayout->addWidget( showEroded );
|
||||
|
||||
showFinal = new QCheckBox( "Show Final Image", this );
|
||||
showFinal->setChecked( true );
|
||||
settingsLayout->addWidget( showFinal );
|
||||
|
||||
settingsLayout->addStretch();
|
||||
|
||||
graphicsView = new QGraphicsView( this );
|
||||
graphicsScene = new QGraphicsScene( this );
|
||||
graphicsView->setScene( graphicsScene );
|
||||
centerLayout->addWidget( graphicsView );
|
||||
|
||||
// Give more space to graphics view
|
||||
centerLayout->setStretch( 0, 3 );
|
||||
centerLayout->setStretch( 1, 7 );
|
||||
|
||||
// Create a button box
|
||||
QDialogButtonBox* buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this );
|
||||
mainLayout->addWidget( buttonBox );
|
||||
|
||||
// Connect the button box signals
|
||||
connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
|
||||
connect( buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
|
||||
|
||||
// Connect signals and slots
|
||||
connect( showInput, &QCheckBox::clicked, this, &ImageProcessingDialog::showImages );
|
||||
connect( showEroded, &QCheckBox::clicked, this, &ImageProcessingDialog::showImages );
|
||||
connect( showDilated, &QCheckBox::clicked, this, &ImageProcessingDialog::showImages );
|
||||
connect( showFinal, &QCheckBox::clicked, this, &ImageProcessingDialog::showImages );
|
||||
|
||||
connect( kernelSpinBox, &QSpinBox::valueChanged, this, &ImageProcessingDialog::updateAndShowImages );
|
||||
connect( transparencySlider, &QSlider::valueChanged, this, &ImageProcessingDialog::updateAndShowImages );
|
||||
|
||||
resizeAndCenterDialog( 0.6 ); // 1.0 means to the full screen size
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::performDilation()
|
||||
{
|
||||
if ( sourceData.empty() )
|
||||
{
|
||||
QMessageBox::warning( this, "Error", "No image loaded." );
|
||||
return;
|
||||
}
|
||||
|
||||
int kernelSize = kernelAdjustedSize();
|
||||
dilatedData = RigPolygonTools::dilate( sourceData, kernelSize );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::performErosion()
|
||||
{
|
||||
if ( sourceData.empty() )
|
||||
{
|
||||
QMessageBox::warning( this, "Error", "No image loaded." );
|
||||
return;
|
||||
}
|
||||
|
||||
int kernelSize = kernelAdjustedSize();
|
||||
erodedData = RigPolygonTools::erode( sourceData, kernelSize );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::updateAndShowImages()
|
||||
{
|
||||
performErosion();
|
||||
performDilation();
|
||||
computeFinal();
|
||||
showImages();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::showImages()
|
||||
{
|
||||
if ( sourceData.empty() )
|
||||
{
|
||||
QMessageBox::warning( this, "Error", "No image loaded." );
|
||||
return;
|
||||
}
|
||||
|
||||
const auto transparency = 180;
|
||||
auto original = RicCreateContourMapPolygonTools::convertBinaryToGrayscaleImage( sourceData, transparency );
|
||||
auto dilatedImage = RicCreateContourMapPolygonTools::convertBinaryToImage( dilatedData, QColorConstants::Green, transparency );
|
||||
auto erodedImage = RicCreateContourMapPolygonTools::convertBinaryToImage( erodedData, QColorConstants::Red, transparency );
|
||||
auto finalImage = RicCreateContourMapPolygonTools::convertBinaryToImage( processedData, QColorConstants::Yellow, transparency );
|
||||
|
||||
graphicsScene->clear();
|
||||
|
||||
// Add a black background image
|
||||
if ( !original.isNull() )
|
||||
{
|
||||
QImage blackImage( original.size(), QImage::Format_RGB32 );
|
||||
blackImage.fill( Qt::black );
|
||||
QPixmap basePixmap = QPixmap::fromImage( blackImage );
|
||||
QGraphicsPixmapItem* baseItem = new QGraphicsPixmapItem( basePixmap );
|
||||
graphicsScene->addItem( baseItem );
|
||||
}
|
||||
|
||||
double alpha = transparencySlider->value() / 100.0;
|
||||
|
||||
if ( showInput->isChecked() )
|
||||
{
|
||||
QPixmap basePixmap = QPixmap::fromImage( original );
|
||||
QGraphicsPixmapItem* baseItem = new QGraphicsPixmapItem( basePixmap );
|
||||
baseItem->setOpacity( alpha );
|
||||
graphicsScene->addItem( baseItem );
|
||||
}
|
||||
|
||||
if ( showDilated->isChecked() && !dilatedImage.isNull() )
|
||||
{
|
||||
QPixmap dilatePixmap = QPixmap::fromImage( dilatedImage );
|
||||
QGraphicsPixmapItem* dilateItem = new QGraphicsPixmapItem( dilatePixmap );
|
||||
dilateItem->setOpacity( alpha );
|
||||
graphicsScene->addItem( dilateItem );
|
||||
}
|
||||
|
||||
if ( showEroded->isChecked() && !erodedImage.isNull() )
|
||||
{
|
||||
QPixmap erodePixmap = QPixmap::fromImage( erodedImage );
|
||||
QGraphicsPixmapItem* erodeItem = new QGraphicsPixmapItem( erodePixmap );
|
||||
erodeItem->setOpacity( alpha );
|
||||
graphicsScene->addItem( erodeItem );
|
||||
}
|
||||
|
||||
if ( showFinal->isChecked() && !finalImage.isNull() )
|
||||
{
|
||||
QPixmap erodePixmap = QPixmap::fromImage( finalImage );
|
||||
QGraphicsPixmapItem* erodeItem = new QGraphicsPixmapItem( erodePixmap );
|
||||
erodeItem->setOpacity( alpha );
|
||||
graphicsScene->addItem( erodeItem );
|
||||
}
|
||||
|
||||
graphicsView->fitInView( graphicsScene->sceneRect(), Qt::KeepAspectRatio );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::vector<int>> ImageProcessingDialog::processedImageData() const
|
||||
{
|
||||
return processedData;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::setSourceImageData( std::vector<std::vector<int>> imageData )
|
||||
{
|
||||
sourceData = imageData;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int ImageProcessingDialog::kernelAdjustedSize() const
|
||||
{
|
||||
// The kernel size should be odd and positive
|
||||
return kernelSpinBox->value() * 2 - 1;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::computeFinal()
|
||||
{
|
||||
if ( sourceData.empty() )
|
||||
{
|
||||
QMessageBox::warning( this, "Error", "No image loaded." );
|
||||
return;
|
||||
}
|
||||
|
||||
auto floodFilled = RigPolygonTools::fillInterior( sourceData );
|
||||
|
||||
int kernelSize = kernelAdjustedSize();
|
||||
auto eroded = RigPolygonTools::erode( floodFilled, kernelSize );
|
||||
auto dilated = RigPolygonTools::dilate( eroded, kernelSize );
|
||||
processedData = dilated;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::resizeAndCenterDialog( double scale )
|
||||
{
|
||||
QScreen* screen = QGuiApplication::primaryScreen();
|
||||
if ( !screen ) return;
|
||||
|
||||
QRect screenGeometry = screen->geometry();
|
||||
|
||||
int width = static_cast<int>( screenGeometry.width() * scale );
|
||||
int height = static_cast<int>( screenGeometry.height() * scale );
|
||||
|
||||
resize( width, height );
|
||||
|
||||
int x = screenGeometry.x() + ( screenGeometry.width() - width ) / 2;
|
||||
int y = screenGeometry.y() + ( screenGeometry.height() - height ) / 2;
|
||||
|
||||
move( x, y );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void ImageProcessingDialog::resizeEvent( QResizeEvent* event )
|
||||
{
|
||||
QDialog::resizeEvent( event );
|
||||
showImages();
|
||||
}
|
73
ApplicationLibCode/Commands/RicPolygonFromImageDialog.h
Normal file
73
ApplicationLibCode/Commands/RicPolygonFromImageDialog.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QFileDialog>
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class ImageProcessingDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ImageProcessingDialog( QWidget* parent = nullptr );
|
||||
|
||||
std::vector<std::vector<int>> processedImageData() const;
|
||||
void setSourceImageData( std::vector<std::vector<int>> imageData );
|
||||
|
||||
public slots:
|
||||
void updateAndShowImages();
|
||||
|
||||
private slots:
|
||||
void performDilation();
|
||||
void performErosion();
|
||||
void showImages();
|
||||
|
||||
private:
|
||||
int kernelAdjustedSize() const;
|
||||
void computeFinal();
|
||||
void resizeAndCenterDialog( double scale );
|
||||
|
||||
void resizeEvent( QResizeEvent* event ) override;
|
||||
|
||||
private:
|
||||
QSpinBox* kernelSpinBox;
|
||||
QSlider* transparencySlider;
|
||||
QGraphicsView* graphicsView;
|
||||
QGraphicsScene* graphicsScene;
|
||||
|
||||
QCheckBox* showInput;
|
||||
QCheckBox* showDilated;
|
||||
QCheckBox* showEroded;
|
||||
QCheckBox* showFinal;
|
||||
|
||||
std::vector<std::vector<int>> sourceData, dilatedData, erodedData, processedData;
|
||||
};
|
@@ -77,7 +77,7 @@ RimGeoMechContourMapView::RimGeoMechContourMapView()
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimGeoMechContourMapProjection* RimGeoMechContourMapView::contourMapProjection() const
|
||||
RimContourMapProjection* RimGeoMechContourMapView::contourMapProjection() const
|
||||
{
|
||||
return m_contourMapProjection().p();
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@
|
||||
|
||||
enum class RimLegendConfigChangeType;
|
||||
class RimGeoMechContourMapProjection;
|
||||
class RimContourMapProjection;
|
||||
class RimRegularLegendConfig;
|
||||
class RimViewNameConfig;
|
||||
class RimScaleLegendConfig;
|
||||
@@ -34,7 +35,7 @@ class RimGeoMechContourMapView : public RimGeoMechView
|
||||
|
||||
public:
|
||||
RimGeoMechContourMapView();
|
||||
RimGeoMechContourMapProjection* contourMapProjection() const;
|
||||
RimContourMapProjection* contourMapProjection() const;
|
||||
|
||||
RiaDefines::View3dContent viewContent() const override;
|
||||
|
||||
|
@@ -52,7 +52,6 @@
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimFaultInViewCollection.h"
|
||||
#include "RimGeoMechCase.h"
|
||||
#include "RimGeoMechContourMapProjection.h"
|
||||
#include "RimGeoMechContourMapView.h"
|
||||
#include "RimGeoMechResultDefinition.h"
|
||||
#include "RimGeoMechView.h"
|
||||
|
@@ -33,7 +33,6 @@
|
||||
#include "RimEclipseContourMapView.h"
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimGeoMechCase.h"
|
||||
#include "RimGeoMechContourMapProjection.h"
|
||||
#include "RimGeoMechContourMapView.h"
|
||||
#include "RimGeoMechResultDefinition.h"
|
||||
#include "RimGeoMechView.h"
|
||||
|
@@ -20,6 +20,7 @@
|
||||
#include "RiuViewerCommands.h"
|
||||
|
||||
#include "RiaDefines.h"
|
||||
#include "RiaOptionItemFactory.h"
|
||||
|
||||
#include "GeoMechCommands/RicGeoMechPropertyFilterNewExec.h"
|
||||
#include "MeasurementCommands/RicMeasurementPickEventHandler.h"
|
||||
@@ -39,13 +40,13 @@
|
||||
#include "RigMainGrid.h"
|
||||
#include "RigVirtualPerforationTransmissibilities.h"
|
||||
|
||||
#include "RiaOptionItemFactory.h"
|
||||
#include "Rim2dIntersectionView.h"
|
||||
#include "RimBoxIntersection.h"
|
||||
#include "RimCellEdgeColors.h"
|
||||
#include "RimContextCommandBuilder.h"
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimEclipseCellColors.h"
|
||||
#include "RimEclipseContourMapView.h"
|
||||
#include "RimEclipseFaultColors.h"
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimEllipseFractureTemplate.h"
|
||||
@@ -55,6 +56,7 @@
|
||||
#include "RimFracture.h"
|
||||
#include "RimGeoMechCase.h"
|
||||
#include "RimGeoMechCellColors.h"
|
||||
#include "RimGeoMechContourMapView.h"
|
||||
#include "RimGeoMechView.h"
|
||||
#include "RimIntersectionResultDefinition.h"
|
||||
#include "RimLegendConfig.h"
|
||||
@@ -593,28 +595,38 @@ void RiuViewerCommands::displayContextMenu( QMouseEvent* event )
|
||||
|
||||
if ( gridView )
|
||||
{
|
||||
menuBuilder.addSeparator();
|
||||
menuBuilder << "RicNewGridTimeHistoryCurveFeature";
|
||||
menuBuilder << "RicShowFlowCharacteristicsPlotFeature";
|
||||
if ( dynamic_cast<RimEclipseView*>( gridView ) )
|
||||
bool isContourView = dynamic_cast<RimEclipseContourMapView*>( gridView ) || dynamic_cast<RimGeoMechContourMapView*>( gridView );
|
||||
if ( isContourView )
|
||||
{
|
||||
menuBuilder << "RicCreateGridCrossPlotFeature";
|
||||
menuBuilder << "RicCreateContourMapPolygonFeature";
|
||||
menuBuilder << "RicCreateContourMapPolygonAdvancedFeature";
|
||||
menuBuilder.addSeparator();
|
||||
menuBuilder << "RicExportContourMapToTextFeature";
|
||||
}
|
||||
menuBuilder.addSeparator();
|
||||
menuBuilder.subMenuStart( "Export" );
|
||||
menuBuilder << "RicExportEclipseInputGridFeature";
|
||||
menuBuilder << "RicSaveEclipseInputActiveVisibleCellsFeature";
|
||||
menuBuilder << "RicSaveEclipseResultAsInputPropertyFeature";
|
||||
menuBuilder << "RicExportContourMapToTextFeature";
|
||||
menuBuilder.subMenuEnd();
|
||||
menuBuilder.addSeparator();
|
||||
else
|
||||
{
|
||||
menuBuilder.addSeparator();
|
||||
menuBuilder << "RicNewGridTimeHistoryCurveFeature";
|
||||
menuBuilder << "RicShowFlowCharacteristicsPlotFeature";
|
||||
if ( dynamic_cast<RimEclipseView*>( gridView ) )
|
||||
{
|
||||
menuBuilder << "RicCreateGridCrossPlotFeature";
|
||||
}
|
||||
menuBuilder.addSeparator();
|
||||
menuBuilder.subMenuStart( "Export" );
|
||||
menuBuilder << "RicExportEclipseInputGridFeature";
|
||||
menuBuilder << "RicSaveEclipseInputActiveVisibleCellsFeature";
|
||||
menuBuilder << "RicSaveEclipseResultAsInputPropertyFeature";
|
||||
menuBuilder.subMenuEnd();
|
||||
menuBuilder.addSeparator();
|
||||
|
||||
#ifdef USE_QTCHARTS
|
||||
menuBuilder << "RicCreateGridStatisticsPlotFeature";
|
||||
menuBuilder << "RicCreateGridStatisticsPlotFeature";
|
||||
#endif
|
||||
menuBuilder << "RicShowGridStatisticsFeature";
|
||||
menuBuilder << "RicCopyGridStatisticsToClipboardFeature";
|
||||
menuBuilder << "RicSelectColorResult";
|
||||
menuBuilder << "RicShowGridStatisticsFeature";
|
||||
menuBuilder << "RicCopyGridStatisticsToClipboardFeature";
|
||||
menuBuilder << "RicSelectColorResult";
|
||||
}
|
||||
}
|
||||
|
||||
if ( firstHitPart )
|
||||
|
Reference in New Issue
Block a user