diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicSimplifyPolygonFeature.cpp b/ApplicationLibCode/Commands/PolygonCommands/RicSimplifyPolygonFeature.cpp index 786afdd797..d71b518031 100644 --- a/ApplicationLibCode/Commands/PolygonCommands/RicSimplifyPolygonFeature.cpp +++ b/ApplicationLibCode/Commands/PolygonCommands/RicSimplifyPolygonFeature.cpp @@ -21,7 +21,7 @@ #include "Polygons/RimPolygon.h" #include "Polygons/RimPolygonInView.h" -#include "RigCellGeometryTools.h" +#include "RigPolygonTools.h" #include "cafSelectionManager.h" @@ -46,21 +46,47 @@ void RicSimplifyPolygonFeature::onActionTriggered( bool isChecked ) auto selPolygons = selectedPolygons(); if ( selPolygons.empty() ) return; - const double defaultEpsilon = 10.0; - - bool ok; - auto epsilon = - QInputDialog::getDouble( nullptr, "Simplify Polygon Threshold", "Threshold:", defaultEpsilon, 1.0, 1000.0, 1, &ok, Qt::WindowFlags(), 1 ); - - if ( !ok ) return; + std::vector> originalCoords; for ( auto sourcePolygon : selPolygons ) { - auto coords = sourcePolygon->pointsInDomainCoords(); - RigCellGeometryTools::simplifyPolygon( &coords, epsilon ); + originalCoords.push_back( sourcePolygon->pointsInDomainCoords() ); + } - sourcePolygon->setPointsInDomainCoords( coords ); - sourcePolygon->coordinatesChanged.send(); + const int defaultEpsilon = 10; + + QInputDialog inputDialog; + inputDialog.setWindowTitle( "Simplify Polygon" ); + inputDialog.setLabelText( "Threshold (larger value removes more points) :" ); + inputDialog.setInputMode( QInputDialog::IntInput ); + inputDialog.setIntRange( 10, 200 ); + inputDialog.setIntValue( defaultEpsilon ); + + connect( &inputDialog, + &QInputDialog::intValueChanged, + [&originalCoords, &selPolygons]( int value ) + { + for ( size_t i = 0; i < originalCoords.size(); i++ ) + { + auto coords = originalCoords[i]; + RigPolygonTools::simplifyPolygon( coords, value ); + + auto sourcePolygon = selPolygons[i]; + sourcePolygon->setPointsInDomainCoords( coords ); + sourcePolygon->coordinatesChanged.send(); + } + } ); + + if ( inputDialog.exec() == QDialog::Rejected ) + { + for ( size_t i = 0; i < originalCoords.size(); i++ ) + { + auto coords = originalCoords[i]; + + auto sourcePolygon = selPolygons[i]; + sourcePolygon->setPointsInDomainCoords( coords ); + sourcePolygon->coordinatesChanged.send(); + } } } diff --git a/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake b/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake index d598d43bd7..550689489b 100644 --- a/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake +++ b/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake @@ -89,6 +89,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RigEclipseContourMapProjection.h ${CMAKE_CURRENT_LIST_DIR}/RigGeoMechContourMapProjection.h ${CMAKE_CURRENT_LIST_DIR}/RigStatisticsContourMapProjection.h + ${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -180,6 +181,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RigEclipseContourMapProjection.cpp ${CMAKE_CURRENT_LIST_DIR}/RigGeoMechContourMapProjection.cpp ${CMAKE_CURRENT_LIST_DIR}/RigStatisticsContourMapProjection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools.cpp ) list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) diff --git a/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.cpp b/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.cpp index 1fe3549c7d..954f0dd456 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.cpp @@ -233,50 +233,6 @@ void RigCellGeometryTools::createPolygonFromLineSegments( std::list* vertices, double epsilon ) -{ - CVF_ASSERT( vertices ); - if ( vertices->size() < 3 ) return; - - std::pair maxDistPoint( 0u, 0.0 ); - - for ( size_t i = 1; i < vertices->size() - 1; ++i ) - { - cvf::Vec3d v = vertices->at( i ); - double u; - cvf::Vec3d v_proj = cvf::GeometryTools::projectPointOnLine( vertices->front(), vertices->back(), v, &u ); - double distance = ( v_proj - v ).length(); - if ( distance > maxDistPoint.second ) - { - maxDistPoint = std::make_pair( i, distance ); - } - } - - if ( maxDistPoint.second > epsilon ) - { - std::vector newVertices1( vertices->begin(), vertices->begin() + maxDistPoint.first + 1 ); - std::vector newVertices2( vertices->begin() + maxDistPoint.first, vertices->end() ); - - // Recurse - simplifyPolygon( &newVertices1, epsilon ); - simplifyPolygon( &newVertices2, epsilon ); - - std::vector newVertices( newVertices1.begin(), newVertices1.end() - 1 ); - newVertices.insert( newVertices.end(), newVertices2.begin(), newVertices2.end() ); - *vertices = newVertices; - } - else - { - std::vector newVertices = { vertices->front(), vertices->back() }; - *vertices = newVertices; - } -} - //================================================================================================== /// //================================================================================================== diff --git a/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.h b/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.h index 38eca15ce7..42c93b3277 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.h +++ b/ApplicationLibCode/ReservoirDataModel/RigCellGeometryTools.h @@ -39,7 +39,6 @@ public: static void createPolygonFromLineSegments( std::list>& intersectionLineSegments, std::vector>& polygons, double tolerance = 1.0e-4 ); - static void simplifyPolygon( std::vector* vertices, double epsilon ); static void findCellLocalXYZ( const std::array& hexCorners, cvf::Vec3d& localXdirection, diff --git a/ApplicationLibCode/ReservoirDataModel/RigContourMapGrid.h b/ApplicationLibCode/ReservoirDataModel/RigContourMapGrid.h index 13acdf208d..c310715239 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigContourMapGrid.h +++ b/ApplicationLibCode/ReservoirDataModel/RigContourMapGrid.h @@ -48,7 +48,6 @@ public: cvf::Vec2ui numberOfVerticesIJ() const; cvf::uint numberOfCells() const; - cvf::uint numberOfValidCells() const; size_t numberOfVertices() const; cvf::Vec3d origin3d() const; diff --git a/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.cpp b/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.cpp index b74dcdfd51..3300054fc3 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.cpp @@ -121,6 +121,20 @@ double RigContourMapProjection::valueAtVertex( unsigned int i, unsigned int j ) return std::numeric_limits::infinity(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RigContourMapProjection::filteredValueAtVertex( unsigned int i, unsigned int j ) const +{ + size_t index = m_contourMapGrid.vertexIndexFromIJ( i, j ); + if ( index < numberOfVertices() ) + { + auto values = aggregatedVertexResultsFiltered(); + return values.at( index ); + } + return std::numeric_limits::infinity(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -187,6 +201,14 @@ cvf::Vec3d RigContourMapProjection::origin3d() const return m_contourMapGrid.expandedBoundingBox().min(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RigContourMapProjection::topDepthBoundingBox() const +{ + return m_contourMapGrid.expandedBoundingBox().max().z(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -440,9 +462,9 @@ const std::vector& RigContourMapProjection::aggregatedResults() const //-------------------------------------------------------------------------------------------------- std::vector RigContourMapProjection::aggregatedVertexResultsFiltered() const { - std::vector filteredResults = m_aggregatedVertexResults; if ( m_valueFilter ) { + std::vector filteredResults = m_aggregatedVertexResults; std::transform( filteredResults.begin(), filteredResults.end(), filteredResults.begin(), @@ -450,9 +472,10 @@ std::vector RigContourMapProjection::aggregatedVertexResultsFiltered() c return ( value < m_valueFilter->first || value > m_valueFilter->second ) ? std::numeric_limits::infinity() : value; } ); + return filteredResults; } - return filteredResults; + return m_aggregatedVertexResults; } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.h b/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.h index 8d12175f31..7da0ff9af1 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.h +++ b/ApplicationLibCode/ReservoirDataModel/RigContourMapProjection.h @@ -59,6 +59,7 @@ public: cvf::Vec2ui numberOfVerticesIJ() const; double valueAtVertex( unsigned int i, unsigned int j ) const; + double filteredValueAtVertex( unsigned int i, unsigned int j ) const; unsigned int numberOfCells() const; unsigned int numberOfValidCells() const; @@ -66,6 +67,7 @@ public: bool checkForMapIntersection( const cvf::Vec3d& domainPoint3d, cvf::Vec2d* contourMapPoint, double* valueAtPoint ) const; cvf::Vec3d origin3d() const; + double topDepthBoundingBox() const; std::vector xVertexPositions() const; std::vector yVertexPositions() const; diff --git a/ApplicationLibCode/ReservoirDataModel/RigContourMapTrianglesGenerator.cpp b/ApplicationLibCode/ReservoirDataModel/RigContourMapTrianglesGenerator.cpp index ceb67dfbdf..230ba8fc52 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigContourMapTrianglesGenerator.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigContourMapTrianglesGenerator.cpp @@ -23,6 +23,7 @@ #include "RigCellGeometryTools.h" #include "RigContourMapGrid.h" #include "RigContourMapProjection.h" +#include "RigPolygonTools.h" #include "cvfGeometryUtils.h" @@ -323,7 +324,7 @@ std::pair, std::ve for ( RigContourPolygonsTools::ContourPolygon& polygon : contourPolygons[i] ) { - RigCellGeometryTools::simplifyPolygon( &polygon.vertices, simplifyEpsilon ); + RigPolygonTools::simplifyPolygon( polygon.vertices, simplifyEpsilon ); } } diff --git a/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.cpp b/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.cpp new file mode 100644 index 0000000000..9c64021d71 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.cpp @@ -0,0 +1,327 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RigPolygonTools.h" + +#include "cvfGeometryTools.h" + +#include +#include +#include + +namespace RigPolygonTools +{ +namespace internal +{ + // Function to check if a point is valid and within bounds + bool isValid( int x, int y, int rows, int cols, const IntegerImage& image, const IntegerImage& visited ) + { + return x >= 0 && x < rows && y >= 0 && y < cols && image[x][y] == 1 && !visited[x][y]; + } + + bool isValidImage( const IntegerImage& image ) + { + if ( image.empty() ) return false; + auto rowSize = image[0].size(); + for ( const auto& row : image ) + { + if ( row.size() != rowSize ) return false; + } + + return true; + } + + //-------------------------------------------------------------------------------------------------- + /// + //-------------------------------------------------------------------------------------------------- + void floodFill( IntegerImage& image, int x, int y, int oldColor, int newColor ) + { + if ( !isValidImage( image ) ) return; + + auto rows = static_cast( image.size() ); + auto cols = static_cast( image[0].size() ); + std::stack> stack; + stack.push( { x, y } ); + + while ( !stack.empty() ) + { + auto [cx, cy] = stack.top(); + stack.pop(); + + if ( cx < 0 || cy < 0 || cx >= rows || cy >= cols || image[cx][cy] != oldColor ) continue; + + image[cx][cy] = newColor; + + stack.push( { cx + 1, cy } ); + stack.push( { cx - 1, cy } ); + stack.push( { cx, cy + 1 } ); + stack.push( { cx, cy - 1 } ); + } + } + +}; // namespace internal + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +IntegerImage erode( IntegerImage image, int kernelSize ) +{ + if ( !internal::isValidImage( image ) ) return {}; + if ( kernelSize <= 0 ) return {}; + + auto rows = static_cast( image.size() ); + auto cols = static_cast( image[0].size() ); + int offset = kernelSize / 2; + IntegerImage eroded( rows, std::vector( cols, 0 ) ); + + for ( int i = offset; i < rows - offset; ++i ) + { + for ( int j = offset; j < cols - offset; ++j ) + { + bool erodePixel = true; + for ( int ki = -offset; ki <= offset; ++ki ) + { + for ( int kj = -offset; kj <= offset; ++kj ) + { + if ( image[i + ki][j + kj] == 0 ) + { + erodePixel = false; + break; + } + } + if ( !erodePixel ) break; + } + eroded[i][j] = erodePixel ? 1 : 0; + } + } + + return eroded; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +IntegerImage dilate( IntegerImage image, int kernelSize ) +{ + if ( !internal::isValidImage( image ) ) return {}; + if ( kernelSize <= 0 ) return {}; + + auto rows = static_cast( image.size() ); + auto cols = static_cast( image[0].size() ); + int offset = kernelSize / 2; + IntegerImage dilated( rows, std::vector( cols, 0 ) ); + + for ( int i = offset; i < rows - offset; ++i ) + { + for ( int j = offset; j < cols - offset; ++j ) + { + bool dilatePixel = false; + for ( int ki = -offset; ki <= offset; ++ki ) + { + for ( int kj = -offset; kj <= offset; ++kj ) + { + if ( image[i + ki][j + kj] == 1 ) + { + dilatePixel = true; + break; + } + } + if ( dilatePixel ) break; + } + dilated[i][j] = dilatePixel ? 1 : 0; + } + } + + return dilated; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +IntegerImage fillInterior( IntegerImage sourceImage ) +{ + if ( !internal::isValidImage( sourceImage ) ) return {}; + + auto image = sourceImage; + auto rows = static_cast( image.size() ); + auto cols = static_cast( image[0].size() ); + + // Flood fill the exterior (starting from the borders) + for ( int i = 0; i < rows; ++i ) + { + if ( image[i][0] == 0 ) internal::floodFill( image, i, 0, 0, -1 ); + if ( image[i][cols - 1] == 0 ) internal::floodFill( image, i, cols - 1, 0, -1 ); + } + for ( int j = 0; j < cols; ++j ) + { + if ( image[0][j] == 0 ) internal::floodFill( image, 0, j, 0, -1 ); + if ( image[rows - 1][j] == 0 ) internal::floodFill( image, rows - 1, j, 0, -1 ); + } + + // Fill interior holes (remaining 0s) + for ( int i = 0; i < rows; ++i ) + { + for ( int j = 0; j < cols; ++j ) + { + if ( image[i][j] == 0 ) image[i][j] = 1; + } + } + + // Restore the exterior + for ( int i = 0; i < rows; ++i ) + { + for ( int j = 0; j < cols; ++j ) + { + if ( image[i][j] == -1 ) image[i][j] = 0; + } + } + + return image; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector> boundary( const IntegerImage& image ) +{ + if ( !internal::isValidImage( image ) ) return {}; + + std::vector> boundaries; + + // Get dimensions of the image + int rows = static_cast( image.size() ); + int cols = static_cast( image[0].size() ); + + // Direction vectors for clockwise search (8-connectivity) + const std::vector> directions = { { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { 0, -1 }, { -1, -1 } }; + + // Helper lambda to check if a pixel is a valid boundary pixel + auto isBoundaryPixel = [&]( int x, int y ) + { + if ( x < 0 || x >= rows || y < 0 || y >= cols || image[x][y] == 0 ) return false; + // Check if it's adjacent to a background pixel + for ( const auto& [dx, dy] : directions ) + { + int nx = x + dx, ny = y + dy; + if ( nx < 0 || nx >= rows || ny < 0 || ny >= cols || image[nx][ny] == 0 ) + { + return true; + } + } + return false; + }; + + // Find the starting boundary pixel + std::pair start( -1, -1 ); + for ( int row = 0; row < rows; ++row ) + { + for ( int col = 0; col < cols; ++col ) + { + if ( isBoundaryPixel( row, col ) ) + { + start = { row, col }; + break; + } + } + if ( start.first != -1 ) break; + } + + if ( start.first == -1 ) return boundaries; // No boundary found + + // Contour following algorithm + std::pair current = start; + int direction = 0; // Start search direction (arbitrary) + do + { + boundaries.push_back( current ); + bool foundNext = false; + + // Look for the next boundary pixel in a clockwise direction + for ( int i = 0; i < 8; ++i ) + { + int newDir = ( direction + i ) % 8; + int nx = current.first + directions[newDir].first; + int ny = current.second + directions[newDir].second; + + if ( isBoundaryPixel( nx, ny ) ) + { + current = { nx, ny }; + direction = ( newDir + 6 ) % 8; // Adjust direction for next search + foundNext = true; + break; + } + } + + // If no next pixel is found, the boundary is invalid or incomplete + if ( !foundNext ) break; + + } while ( current != start ); // Stop when we loop back to the start + + return boundaries; +} + +//-------------------------------------------------------------------------------------------------- +/// Ramer-Douglas-Peucker simplification algorithm +/// +/// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm +//-------------------------------------------------------------------------------------------------- +void simplifyPolygon( std::vector& vertices, double epsilon ) +{ + // If the polygon has fewer than 3 vertices, it cannot be simplified. + if ( vertices.size() < 3 ) return; + + // Find the point with the maximum perpendicular distance from the line connecting the endpoints. + std::optional> maxDistPoint; + + for ( size_t i = 1; i < vertices.size() - 1; ++i ) + { + const cvf::Vec3d& point = vertices[i]; + cvf::Vec3d projected = cvf::GeometryTools::projectPointOnLine( vertices.front(), vertices.back(), point ); + double distance = ( projected - point ).length(); + + if ( !maxDistPoint || distance > maxDistPoint->second ) + { + maxDistPoint = std::make_pair( i, distance ); + } + } + + // If the maximum distance exceeds epsilon, split and simplify recursively. + if ( maxDistPoint && maxDistPoint->second > epsilon ) + { + size_t splitIndex = maxDistPoint->first; + + // Divide the vertices into two segments. + std::vector segment1( vertices.begin(), vertices.begin() + splitIndex + 1 ); + std::vector segment2( vertices.begin() + splitIndex, vertices.end() ); + + // Recursively simplify both segments. + simplifyPolygon( segment1, epsilon ); + simplifyPolygon( segment2, epsilon ); + + // Combine the simplified segments, avoiding duplication at the split point. + vertices = std::move( segment1 ); + vertices.pop_back(); // Remove duplicate at the split point. + vertices.insert( vertices.end(), segment2.begin(), segment2.end() ); + } + else + { + // If no point exceeds the threshold, reduce to endpoints. + vertices = { vertices.front(), vertices.back() }; + } +} +} // namespace RigPolygonTools diff --git a/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.h b/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.h new file mode 100644 index 0000000000..3b08ba48b7 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/RigPolygonTools.h @@ -0,0 +1,39 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cvfVector3.h" + +#include + +namespace RigPolygonTools +{ +// Integer images are assumed to be 2D arrays of integers, where 0 is background and 1 is foreground. +using IntegerImage = std::vector>; + +IntegerImage erode( IntegerImage image, int kernelSize ); +IntegerImage dilate( IntegerImage image, int kernelSize ); +IntegerImage fillInterior( IntegerImage sourceImage ); + +std::vector> boundary( const IntegerImage& image ); + +// Recursive function modifying the incoming vertices +void simplifyPolygon( std::vector& vertices, double epsilon ); + +} // namespace RigPolygonTools diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index eeaf6cd846..b75f7c5e48 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -106,6 +106,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RifOsduWellPathReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigVfpTables-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaResultName-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools-Test.cpp ) if(RESINSIGHT_ENABLE_GRPC) diff --git a/ApplicationLibCode/UnitTests/RigPolygonTools-Test.cpp b/ApplicationLibCode/UnitTests/RigPolygonTools-Test.cpp new file mode 100644 index 0000000000..2652d933ab --- /dev/null +++ b/ApplicationLibCode/UnitTests/RigPolygonTools-Test.cpp @@ -0,0 +1,173 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2020- 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 "RigPolygonTools.h" +#include "gtest/gtest.h" + +// Test for erode function +TEST( RigPolygonToolsTest, ErodeTest ) +{ + // Arrange + RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } }; + int kernelSize = 1; + RigPolygonTools::IntegerImage expected = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } }; + + // Act + RigPolygonTools::IntegerImage result = RigPolygonTools::erode( image, kernelSize ); + + // Assert + EXPECT_EQ( result, expected ); +} + +// Test for erode function with invalid input +TEST( RigPolygonToolsTest, ErodeInvalidInputTest ) +{ + // Arrange + RigPolygonTools::IntegerImage emptyImage = {}; + int negativeKernelSize = -1; + + // Act & Assert + EXPECT_EQ( emptyImage, RigPolygonTools::erode( emptyImage, 1 ) ); + EXPECT_EQ( emptyImage, RigPolygonTools::erode( { { 1, 1 }, { 1 } }, 1 ) ); // Inconsistent row sizes + EXPECT_EQ( emptyImage, RigPolygonTools::erode( { { 1, 1 }, { 1, 1 } }, negativeKernelSize ) ); +} + +// Test for dilate function +TEST( RigPolygonToolsTest, DilateTest ) +{ + // Arrange + RigPolygonTools::IntegerImage image = { + { 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0 }, + { 0, 0, 1, 0, 0 }, + { 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0 }, + }; + int kernelSize = 2; + + RigPolygonTools::IntegerImage expected = { + { 0, 0, 0, 0, 0 }, + { 0, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0 }, + { 0, 0, 0, 0, 0 }, + }; + + // Act + RigPolygonTools::IntegerImage result = RigPolygonTools::dilate( image, kernelSize ); + + // Assert + EXPECT_EQ( result, expected ); +} + +// Test for dilate function with invalid input +TEST( RigPolygonToolsTest, DilateInvalidInputTest ) +{ + // Arrange + RigPolygonTools::IntegerImage emptyImage = {}; + int negativeKernelSize = -1; + + // Act & Assert + EXPECT_EQ( emptyImage, RigPolygonTools::dilate( emptyImage, 1 ) ); + EXPECT_EQ( emptyImage, RigPolygonTools::dilate( { { 1, 1 }, { 1 } }, 1 ) ); // Inconsistent row sizes + EXPECT_EQ( emptyImage, RigPolygonTools::dilate( { { 1, 1 }, { 1, 1 } }, negativeKernelSize ) ); +} + +// Test for fillInterior function +TEST( RigPolygonToolsTest, FillInteriorTest ) +{ + // Arrange + RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 0, 1 }, { 1, 1, 1 } }; + RigPolygonTools::IntegerImage expected = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } }; + + // Act + RigPolygonTools::IntegerImage result = RigPolygonTools::fillInterior( image ); + + // Assert + EXPECT_EQ( result, expected ); +} + +// Test for fillInterior function with invalid input +TEST( RigPolygonToolsTest, FillInteriorInvalidInputTest ) +{ + // Arrange + RigPolygonTools::IntegerImage emptyImage = {}; + + // Act & Assert + EXPECT_EQ( emptyImage, RigPolygonTools::fillInterior( emptyImage ) ); + EXPECT_EQ( emptyImage, RigPolygonTools::fillInterior( { { 1, 1 }, { 1 } } ) ); // Inconsistent row sizes +} + +// Test for boundary function +TEST( RigPolygonToolsTest, BoundaryTest ) +{ + // Arrange + RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 0, 1 }, { 1, 1, 1 } }; + std::vector> expected = { { 0, 0 }, { 0, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, { 2, 1 }, { 2, 0 }, { 1, 0 } }; + + // Act + std::vector> result = RigPolygonTools::boundary( image ); + + // Assert + EXPECT_EQ( result, expected ); +} + +// Test for boundary function with invalid input +TEST( RigPolygonToolsTest, BoundaryInvalidInputTest ) +{ + // Arrange + RigPolygonTools::IntegerImage emptyImage = {}; + + // Act & Assert + EXPECT_TRUE( RigPolygonTools::boundary( emptyImage ).empty() ); + EXPECT_TRUE( RigPolygonTools::boundary( { { 1, 1 }, { 1 } } ).empty() ); // Inconsistent row sizes +} + +// Test for simplifyPolygon function +TEST( RigPolygonToolsTest, SimplifyPolygonTest ) +{ + // Arrange + std::vector vertices = + { { 0.0, 0.0, 0.0 }, { 1.0, 0.1, 0.0 }, { 1.5, -0.1, 0.0 }, { 3.0, 5.0, 0.0 }, { 5.0, 6.0, 0.0 }, { 7.0, 7.0, 0.0 }, { 8.0, 8.0, 0.0 } }; + double epsilon = 1; + std::vector expected = { { 0.0, 0.0, 0.0 }, { 1.5, -0.1, 0.0 }, { 3.0, 5.0, 0.0 }, { 8.0, 8.0, 0.0 } }; + + // Act + RigPolygonTools::simplifyPolygon( vertices, epsilon ); + + // Assert + EXPECT_EQ( vertices.size(), expected.size() ); + for ( size_t i = 0; i < vertices.size(); ++i ) + { + EXPECT_DOUBLE_EQ( vertices[i].x(), expected[i].x() ); + EXPECT_DOUBLE_EQ( vertices[i].y(), expected[i].y() ); + EXPECT_DOUBLE_EQ( vertices[i].z(), expected[i].z() ); + } +} + +// Test for simplifyPolygon function with invalid input +TEST( RigPolygonToolsTest, SimplifyPolygonInvalidInputTest ) +{ + // Arrange + std::vector emptyVertices; + double epsilon = 1.0; + + // Act & Assert + EXPECT_NO_THROW( RigPolygonTools::simplifyPolygon( emptyVertices, epsilon ) ); + EXPECT_EQ( emptyVertices.size(), 0 ); +}