diff --git a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake index 600b413e58..f8c5f5e4df 100644 --- a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake +++ b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake @@ -107,6 +107,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RifVtkSurfaceImporter.h ${CMAKE_CURRENT_LIST_DIR}/RifSurfio.h ${CMAKE_CURRENT_LIST_DIR}/RifFileTools.h + ${CMAKE_CURRENT_LIST_DIR}/RifVtkImportUtil.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -211,6 +212,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RifVtkSurfaceImporter.cpp ${CMAKE_CURRENT_LIST_DIR}/RifSurfio.cpp ${CMAKE_CURRENT_LIST_DIR}/RifFileTools.cpp + ${CMAKE_CURRENT_LIST_DIR}/RifVtkImportUtil.cpp ) list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) diff --git a/ApplicationLibCode/FileInterface/RifVtkImportUtil.cpp b/ApplicationLibCode/FileInterface/RifVtkImportUtil.cpp new file mode 100644 index 0000000000..2d9e98ee5f --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifVtkImportUtil.cpp @@ -0,0 +1,209 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RifVtkImportUtil.h" + +#include "RiaStdStringTools.h" + +#include +#include +#include +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RifVtkImportUtil::readPoints( const pugi::xml_node& piece ) +{ + auto points = piece.child( "Points" ); + if ( !points ) return {}; + + auto coords = points.child( "DataArray" ); + if ( !coords || std::string( coords.attribute( "Name" ).value() ) != "Coordinates" ) return {}; + + std::string_view text = coords.text().get(); + std::vector vertices = parseVec3ds( text ); + + // Update to internal z direction + for ( cvf::Vec3d& v : vertices ) + v.z() = -v.z(); + + return vertices; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RifVtkImportUtil::readConnectivity( const pugi::xml_node& piece ) +{ + auto cells = piece.child( "Cells" ); + if ( !cells ) return {}; + + std::vector connectivity; + auto connectivityArray = cells.child( "DataArray" ); + + while ( connectivityArray ) + { + if ( std::string( connectivityArray.attribute( "Name" ).value() ) == "connectivity" ) + { + std::string connectivityText = connectivityArray.text().get(); + std::istringstream iss( connectivityText ); + + unsigned index; + while ( iss >> index ) + { + connectivity.push_back( index ); + } + break; + } + connectivityArray = connectivityArray.next_sibling( "DataArray" ); + } + + return connectivity; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::map> RifVtkImportUtil::readProperties( const pugi::xml_node& piece ) +{ + auto cellData = piece.child( "CellData" ); + if ( !cellData ) return {}; + + std::map> properties; + + for ( const auto& dataArray : cellData.children( "DataArray" ) ) + { + const char* name = dataArray.attribute( "Name" ).value(); + if ( name && *name ) + { + std::vector values; + std::istringstream iss( dataArray.text().get() ); + + std::string token; + while ( iss >> token ) + { + if ( token == "nan" || token == "NaN" || token == "NAN" ) + { + values.push_back( std::numeric_limits::quiet_NaN() ); + } + else + { + double value = 0.0; + bool isOk = RiaStdStringTools::toDouble( token, value ); + if ( isOk ) + values.push_back( static_cast( value ) ); + else + return {}; + } + } + + if ( !values.empty() ) + { + properties[name] = std::move( values ); + } + } + } + + return properties; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RifVtkImportUtil::parsePvdDatasets( const std::filesystem::path& filepath ) +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file( filepath.string().c_str() ); + if ( !result ) return {}; + + auto root = doc.child( "VTKFile" ); + if ( !root ) return {}; + + auto collection = root.child( "Collection" ); + if ( !collection ) return {}; + + std::filesystem::path baseDir = filepath.parent_path(); + + std::vector datasets; + + for ( const auto& datasetElem : collection.children( "DataSet" ) ) + { + const char* file = datasetElem.attribute( "file" ).value(); + const char* timestepStr = datasetElem.attribute( "timestep" ).value(); + + if ( file && timestepStr ) + { + double timestep = std::stod( timestepStr ); + std::filesystem::path fullPath = std::filesystem::absolute( baseDir / file ); + + datasets.push_back( { timestep, fullPath } ); + } + } + + return datasets; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RifVtkImportUtil::parseVec3ds( std::string_view text ) +{ + std::ispanstream iss( text ); + + std::vector vecs; + + std::string xStr, yStr, zStr; + while ( iss >> xStr >> yStr >> zStr ) + { + double x, y, z; + if ( !RiaStdStringTools::toDouble( xStr, x ) || !RiaStdStringTools::toDouble( yStr, y ) || !RiaStdStringTools::toDouble( zStr, z ) ) + { + return {}; + } + + vecs.emplace_back( x, y, z ); + } + + return vecs; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RifVtkImportUtil::parseVec3fs( std::string_view text ) +{ + std::ispanstream iss( text ); + + std::vector vecs; + + std::string xStr, yStr, zStr; + while ( iss >> xStr >> yStr >> zStr ) + { + double x, y, z; + if ( !RiaStdStringTools::toDouble( xStr, x ) || !RiaStdStringTools::toDouble( yStr, y ) || !RiaStdStringTools::toDouble( zStr, z ) ) + { + return {}; + } + + vecs.emplace_back( static_cast( x ), static_cast( y ), static_cast( z ) ); + } + + return vecs; +} diff --git a/ApplicationLibCode/FileInterface/RifVtkImportUtil.h b/ApplicationLibCode/FileInterface/RifVtkImportUtil.h new file mode 100644 index 0000000000..633a044eee --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifVtkImportUtil.h @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +#include +#include +#include + +#include "cvfVector3.h" + +#include "pugixml.hpp" + +//================================================================================================== +/// +//================================================================================================== +class RifVtkImportUtil +{ +public: + struct PvdDataset + { + double timestep; + std::filesystem::path filepath; + }; + + static std::vector parsePvdDatasets( const std::filesystem::path& filepath ); + + static std::vector readPoints( const pugi::xml_node& piece ); + static std::vector readConnectivity( const pugi::xml_node& piece ); + static std::map> readProperties( const pugi::xml_node& piece ); + +private: + static std::vector parseVec3ds( std::string_view text ); + static std::vector parseVec3fs( std::string_view text ); +}; diff --git a/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.cpp b/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.cpp index e43c3a67f0..5a80daf9b7 100644 --- a/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.cpp +++ b/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.cpp @@ -18,11 +18,12 @@ #include "RifVtkSurfaceImporter.h" +#include "RifVtkImportUtil.h" + #include "Surface/RigTriangleMeshData.h" #include #include -#include #include #include @@ -53,11 +54,11 @@ std::unique_ptr RifVtkSurfaceImporter::importFromXmlDoc( co if ( !piece ) return nullptr; // Read points - std::vector vertices = readPoints( piece ); + std::vector vertices = RifVtkImportUtil::readPoints( piece ); if ( vertices.empty() ) return nullptr; // Read connectivity - std::vector connectivity = readConnectivity( piece ); + std::vector connectivity = RifVtkImportUtil::readConnectivity( piece ); if ( connectivity.empty() ) return nullptr; // Avoid shared nodes @@ -81,7 +82,7 @@ std::unique_ptr RifVtkSurfaceImporter::importFromXmlDoc( co triangleMeshData->setGeometryData( nonSharedVertices, nonSharedConnectivity ); // Read properties - const std::map> properties = readProperties( piece ); + const std::map> properties = RifVtkImportUtil::readProperties( piece ); for ( const auto& [name, values] : properties ) { // These values are per element, so we need to duplicate them for each node @@ -101,148 +102,3 @@ std::unique_ptr RifVtkSurfaceImporter::importFromXmlDoc( co return triangleMeshData; } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RifVtkSurfaceImporter::readPoints( const pugi::xml_node& piece ) -{ - auto points = piece.child( "Points" ); - if ( !points ) return {}; - - auto coords = points.child( "DataArray" ); - if ( !coords || std::string( coords.attribute( "Name" ).value() ) != "Coordinates" ) return {}; - - std::vector vertices; - std::istringstream iss( coords.text().get() ); - - double x, y, z; - while ( iss >> x >> y >> z ) - { - vertices.emplace_back( x, y, -z ); - } - - return vertices; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RifVtkSurfaceImporter::readConnectivity( const pugi::xml_node& piece ) -{ - auto cells = piece.child( "Cells" ); - if ( !cells ) return {}; - - std::vector connectivity; - auto connectivityArray = cells.child( "DataArray" ); - - while ( connectivityArray ) - { - if ( std::string( connectivityArray.attribute( "Name" ).value() ) == "connectivity" ) - { - std::string connectivityText = connectivityArray.text().get(); - std::istringstream iss( connectivityText ); - - unsigned index; - while ( iss >> index ) - { - connectivity.push_back( index ); - } - break; - } - connectivityArray = connectivityArray.next_sibling( "DataArray" ); - } - - return connectivity; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::map> RifVtkSurfaceImporter::readProperties( const pugi::xml_node& piece ) -{ - auto cellData = piece.child( "CellData" ); - if ( !cellData ) return {}; - - std::map> properties; - - for ( const auto& dataArray : cellData.children( "DataArray" ) ) - { - const char* name = dataArray.attribute( "Name" ).value(); - if ( name && *name ) - { - std::vector values; - std::istringstream iss( dataArray.text().get() ); - - std::string token; - while ( iss >> token ) - { - if ( token == "nan" || token == "NaN" || token == "NAN" ) - { - values.push_back( std::numeric_limits::quiet_NaN() ); - } - else - { - try - { - float value = std::stof( token ); - values.push_back( value ); - } - catch ( const std::invalid_argument& ) - { - // Quit parsing when encountering non-numeric tokens - return {}; - } - catch ( const std::out_of_range& ) - { - // Quit parsing when seeing out-of-range errors - return {}; - } - } - } - - if ( !values.empty() ) - { - properties[name] = std::move( values ); - } - } - } - - return properties; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RifVtkSurfaceImporter::parsePvdDatasets( const std::filesystem::path& filepath ) -{ - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file( filepath.string().c_str() ); - if ( !result ) return {}; - - auto root = doc.child( "VTKFile" ); - if ( !root ) return {}; - - auto collection = root.child( "Collection" ); - if ( !collection ) return {}; - - std::filesystem::path baseDir = filepath.parent_path(); - - std::vector datasets; - - for ( const auto& datasetElem : collection.children( "DataSet" ) ) - { - const char* file = datasetElem.attribute( "file" ).value(); - const char* timestepStr = datasetElem.attribute( "timestep" ).value(); - - if ( file && timestepStr ) - { - double timestep = std::stod( timestepStr ); - std::filesystem::path fullPath = std::filesystem::absolute( baseDir / file ); - - datasets.push_back( { timestep, fullPath } ); - } - } - - return datasets; -} diff --git a/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.h b/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.h index 3e93e2ad88..2084498b82 100644 --- a/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.h +++ b/ApplicationLibCode/FileInterface/RifVtkSurfaceImporter.h @@ -19,12 +19,7 @@ #pragma once #include -#include #include -#include -#include - -#include "cvfVector3.h" #include "pugixml.hpp" @@ -36,19 +31,8 @@ class RigTriangleMeshData; class RifVtkSurfaceImporter { public: - struct PvdDataset - { - double timestep; - std::filesystem::path filepath; - }; - static std::unique_ptr importFromFile( const std::filesystem::path& filepath ); - static std::vector parsePvdDatasets( const std::filesystem::path& filepath ); - private: - static std::unique_ptr importFromXmlDoc( const pugi::xml_document& doc ); - static std::vector readPoints( const pugi::xml_node& piece ); - static std::vector readConnectivity( const pugi::xml_node& piece ); - static std::map> readProperties( const pugi::xml_node& piece ); + static std::unique_ptr importFromXmlDoc( const pugi::xml_document& doc ); }; diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimFractureSurface.cpp b/ApplicationLibCode/ProjectDataModel/Surfaces/RimFractureSurface.cpp index 3df79c7935..e50eb1adfa 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimFractureSurface.cpp +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimFractureSurface.cpp @@ -28,6 +28,7 @@ #include "RimSurfaceCollection.h" #include "RifSurfaceImporter.h" +#include "RifVtkImportUtil.h" #include "RifVtkSurfaceImporter.h" #include "cafPdmObjectScriptingCapability.h" @@ -199,7 +200,7 @@ void RimFractureSurface::clearCachedNativeData() bool RimFractureSurface::loadDataFromFile() { std::filesystem::path filepath = m_surfaceDefinitionFilePath().path().toLocal8Bit().constData(); - auto surfaceInfo = RifVtkSurfaceImporter::parsePvdDatasets( filepath ); + auto surfaceInfo = RifVtkImportUtil::parsePvdDatasets( filepath ); for ( const auto& s : surfaceInfo ) { diff --git a/ApplicationLibCode/UnitTests/RifVtkSurfaceImporter-Test.cpp b/ApplicationLibCode/UnitTests/RifVtkSurfaceImporter-Test.cpp index 466106b139..ffac1f271d 100644 --- a/ApplicationLibCode/UnitTests/RifVtkSurfaceImporter-Test.cpp +++ b/ApplicationLibCode/UnitTests/RifVtkSurfaceImporter-Test.cpp @@ -1,3 +1,4 @@ +#include "RifVtkImportUtil.h" #include "RifVtkSurfaceImporter.h" #include "RiaTestDataDirectory.h" @@ -59,7 +60,7 @@ TEST( RifVtkSurfaceImporterTest, ParsePvdDatasets ) std::filesystem::path pvdFilePath = testDataDir / "dataset.pvd"; // Exercise - auto datasets = RifVtkSurfaceImporter::parsePvdDatasets( pvdFilePath ); + auto datasets = RifVtkImportUtil::parsePvdDatasets( pvdFilePath ); // Verify ASSERT_EQ( datasets.size(), 2 ) << "Expected 2 datasets in the PVD file"; @@ -85,7 +86,7 @@ TEST( RifVtkSurfaceImporterTest, ImportMultipleTimesteps ) std::filesystem::path pvdFilePath = testDataDir / "dataset.pvd"; // Exercise - auto datasets = RifVtkSurfaceImporter::parsePvdDatasets( pvdFilePath ); + auto datasets = RifVtkImportUtil::parsePvdDatasets( pvdFilePath ); ASSERT_FALSE( datasets.empty() ) << "Failed to parse PVD datasets"; // Import each VTU file @@ -125,7 +126,7 @@ TEST( RifVtkSurfaceImporterTest, HandleInvalidFiles ) // Test with invalid PVD file std::filesystem::path invalidPvdFile = testDataDir / "invalid.pvd"; - auto datasets = RifVtkSurfaceImporter::parsePvdDatasets( invalidPvdFile ); + auto datasets = RifVtkImportUtil::parsePvdDatasets( invalidPvdFile ); EXPECT_TRUE( datasets.empty() ) << "Expected empty dataset list for invalid PVD file"; } @@ -137,7 +138,7 @@ TEST( RifVtkSurfaceImporterTest, PvdPathResolution ) std::filesystem::path pvdFilePath = testDataDir / "dataset.pvd"; // Exercise - auto datasets = RifVtkSurfaceImporter::parsePvdDatasets( pvdFilePath ); + auto datasets = RifVtkImportUtil::parsePvdDatasets( pvdFilePath ); ASSERT_FALSE( datasets.empty() ) << "Failed to parse PVD datasets"; // Verify