From d7ab989587d7aa9b199f57008f9e2bb0270359b4 Mon Sep 17 00:00:00 2001 From: Peter Petrik Date: Fri, 4 Jan 2019 18:18:34 +0100 Subject: [PATCH 1/2] update to MDAL 0.1.3 (mesh calculator API) --- external/mdal/api/mdal.h | 88 ++++++++- external/mdal/frmts/mdal_2dm.cpp | 13 +- external/mdal/frmts/mdal_3di.cpp | 1 + external/mdal/frmts/mdal_ascii_dat.cpp | 12 +- external/mdal/frmts/mdal_binary_dat.cpp | 133 +++++++++++++- external/mdal/frmts/mdal_binary_dat.hpp | 1 + external/mdal/frmts/mdal_cf.cpp | 4 +- external/mdal/frmts/mdal_driver.cpp | 41 ++++- external/mdal/frmts/mdal_driver.hpp | 32 +++- external/mdal/frmts/mdal_flo2d.cpp | 12 +- external/mdal/frmts/mdal_flo2d.hpp | 2 +- external/mdal/frmts/mdal_gdal.cpp | 4 +- external/mdal/frmts/mdal_hec2d.cpp | 5 +- external/mdal/frmts/mdal_sww.cpp | 7 +- external/mdal/frmts/mdal_xmdf.cpp | 27 +-- external/mdal/frmts/mdal_xmdf.hpp | 2 +- external/mdal/mdal.cpp | 220 ++++++++++++++++++++++- external/mdal/mdal_data_model.cpp | 54 +++++- external/mdal/mdal_data_model.hpp | 26 ++- external/mdal/mdal_driver_manager.cpp | 6 +- external/mdal/mdal_memory_data_model.cpp | 14 +- external/mdal/mdal_memory_data_model.hpp | 3 +- external/mdal/mdal_utils.cpp | 30 +++- external/mdal/mdal_utils.hpp | 4 +- 24 files changed, 666 insertions(+), 75 deletions(-) diff --git a/external/mdal/api/mdal.h b/external/mdal/api/mdal.h index b9edbc871b20..e30d5b2201d9 100644 --- a/external/mdal/api/mdal.h +++ b/external/mdal/api/mdal.h @@ -55,6 +55,7 @@ enum MDAL_Status Err_IncompatibleDataset, Err_IncompatibleDatasetGroup, Err_MissingDriver, + Err_MissingDriverCapability, // Warnings Warn_UnsupportedElement, Warn_InvalidElements, @@ -101,6 +102,9 @@ MDAL_EXPORT DriverH MDAL_driverFromName( const char *name ); */ MDAL_EXPORT bool MDAL_DR_meshLoadCapability( DriverH driver ); +//! Returns whether driver has capability to write/edit dataset (groups) +MDAL_EXPORT bool MDAL_DR_writeDatasetsCapability( DriverH driver ); + /** * Returns name of MDAL driver * not thread-safe and valid only till next call @@ -163,6 +167,37 @@ MDAL_EXPORT int MDAL_M_datasetGroupCount( MeshH mesh ); //! Returns dataset group handle MDAL_EXPORT DatasetGroupH MDAL_M_datasetGroup( MeshH mesh, int index ); +/** + * Adds empty (new) dataset group to the mesh + * This increases dataset group count MDAL_M_datasetGroupCount() by 1 + * + * The Dataset Group is opened in edit mode. + * To persist dataset group, call MDAL_G_closeEditMode(); + * + * It is not possible to read and write to the same group + * at the same time. Finalize edits before reading. + * + * \param mesh mesh handle + * \param driver the driver to use for storing the data + * \param name dataset group name + * \param isOnVertices whether data is defined on vertices + * \param hasScalarData whether data is scalar (false = vector data) + * \param datasetGroupFile file to store the new dataset group + * \returns empty pointer if not possible to create group, otherwise handle to new group + */ +MDAL_EXPORT DatasetGroupH MDAL_M_addDatasetGroup( MeshH mesh, + const char *name, + bool isOnVertices, + bool hasScalarData, + DriverH driver, + const char *datasetGroupFile ); + +/** + * Returns name of MDAL driver + * not thread-safe and valid only till next call + */ +MDAL_EXPORT const char *MDAL_M_driverName( MeshH mesh ); + /////////////////////////////////////////////////////////////////////////////////////// /// MESH VERTICES /////////////////////////////////////////////////////////////////////////////////////// @@ -247,12 +282,24 @@ MDAL_EXPORT const char *MDAL_G_metadataKey( DatasetGroupH group, int index ); */ MDAL_EXPORT const char *MDAL_G_metadataValue( DatasetGroupH group, int index ); +/** + * Adds new metadata to the group + * Group must be in edit mode MDAL_G_isInEditMode() + */ +MDAL_EXPORT void MDAL_G_setMetadata( DatasetGroupH group, const char *key, const char *val ); + /** * Returns dataset group name * not thread-safe and valid only till next call */ MDAL_EXPORT const char *MDAL_G_name( DatasetGroupH group ); +/** + * Returns name of MDAL driver + * not thread-safe and valid only till next call + */ +MDAL_EXPORT const char *MDAL_G_driverName( DatasetGroupH group ); + //! Whether dataset has scalar data associated MDAL_EXPORT bool MDAL_G_hasScalarData( DatasetGroupH group ); @@ -260,11 +307,48 @@ MDAL_EXPORT bool MDAL_G_hasScalarData( DatasetGroupH group ); MDAL_EXPORT bool MDAL_G_isOnVertices( DatasetGroupH group ); /** - * Returns the min and max values of the group + * Returns the minimum and maximum values of the group * Returns NaN on error */ MDAL_EXPORT void MDAL_G_minimumMaximum( DatasetGroupH group, double *min, double *max ); +/** + * Adds empty (new) dataset to the group + * This increases dataset group count MDAL_G_datasetCount() by 1 + * + * The dataset is opened in edit mode. + * To persist dataset, call MDAL_G_closeEditMode() on parent group + * + * Minimum and maximum dataset values are automatically calculated + * + * \param group parent group handle + * \param time time for dataset + * \param values For scalar data on vertices, the size must be vertex count + * For scalar data on faces, the size must be faces count + * For vector data on vertices, the size must be vertex count * 2 (x1, y1, x2, y2, ..., xN, yN) + * For vector data on faces, the size must be faces count * 2 (x1, y1, x2, y2, ..., xN, yN) + * \param active if null pointer, all faces are active. Otherwise size must be equal to face count. + * \returns empty pointer if not possible to create dataset (e.g. group opened in read mode), otherwise handle to new dataset + */ +MDAL_EXPORT DatasetH MDAL_G_addDataset( DatasetGroupH group, + double time, + const double *values, + const int *active + ); + +//! Returns whether dataset group is in edit mode +MDAL_EXPORT bool MDAL_G_isInEditMode( DatasetGroupH group ); + +/** + * Close edit mode for group and all its datasets. + * This may effectively write the data to the files and/or + * reopen the file in read-only mode + * + * When closed, minimum and maximum dataset group values are automatically calculated + */ +MDAL_EXPORT void MDAL_G_closeEditMode( DatasetGroupH group ); + + /////////////////////////////////////////////////////////////////////////////////////// /// DATASETS /////////////////////////////////////////////////////////////////////////////////////// @@ -307,7 +391,7 @@ enum MDAL_DataType MDAL_EXPORT int MDAL_D_data( DatasetH dataset, int indexStart, int count, MDAL_DataType dataType, void *buffer ); /** - * Returns the min and max values of the dataset + * Returns the minimum and maximum values of the dataset * Returns NaN on error */ MDAL_EXPORT void MDAL_D_minimumMaximum( DatasetH dataset, double *min, double *max ); diff --git a/external/mdal/frmts/mdal_2dm.cpp b/external/mdal/frmts/mdal_2dm.cpp index f1fece7fb30c..5f323eedd841 100644 --- a/external/mdal/frmts/mdal_2dm.cpp +++ b/external/mdal/frmts/mdal_2dm.cpp @@ -17,13 +17,20 @@ #include "mdal.h" #include "mdal_utils.hpp" +#define DRIVER_NAME "2DM" + MDAL::Mesh2dm::Mesh2dm( size_t verticesCount, size_t facesCount, size_t faceVerticesMaximumCount, MDAL::BBox extent, const std::string &uri, const std::map vertexIDtoIndex ) - : MemoryMesh( verticesCount, facesCount, faceVerticesMaximumCount, extent, uri ) + : MemoryMesh( DRIVER_NAME, + verticesCount, + facesCount, + faceVerticesMaximumCount, + extent, + uri ) , mVertexIDtoIndex( vertexIDtoIndex ) { } @@ -58,10 +65,10 @@ size_t MDAL::Mesh2dm::vertexIndex( size_t vertexID ) const MDAL::Driver2dm::Driver2dm(): - Driver( "2DM", + Driver( DRIVER_NAME, "2DM Mesh File", "*.2dm", - DriverType::CanReadMeshAndDatasets + Capability::ReadMesh ) { } diff --git a/external/mdal/frmts/mdal_3di.cpp b/external/mdal/frmts/mdal_3di.cpp index b404099ec3fa..a454845e5505 100644 --- a/external/mdal/frmts/mdal_3di.cpp +++ b/external/mdal/frmts/mdal_3di.cpp @@ -126,6 +126,7 @@ void MDAL::Driver3Di::addBedElevation( MDAL::Mesh *mesh ) std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mesh, mesh->uri(), "Bed Elevation" diff --git a/external/mdal/frmts/mdal_ascii_dat.cpp b/external/mdal/frmts/mdal_ascii_dat.cpp index 2466923f43e8..025bd48ec593 100644 --- a/external/mdal/frmts/mdal_ascii_dat.cpp +++ b/external/mdal/frmts/mdal_ascii_dat.cpp @@ -27,7 +27,7 @@ MDAL::DriverAsciiDat::DriverAsciiDat( ): Driver( "ASCII_DAT", "DAT", "*.dat", - DriverType::CanReadOnlyDatasets + Capability::ReadDatasets ) { } @@ -93,7 +93,7 @@ void MDAL::DriverAsciiDat::load( const std::string &datFile, MDAL::Mesh *mesh, M bool isVector = false; std::shared_ptr group; // DAT outputs data - std::string name( MDAL::baseName( mDatFile ) ); + std::string groupName( MDAL::baseName( mDatFile ) ); if ( line == "DATASET" ) oldFormat = false; @@ -103,9 +103,10 @@ void MDAL::DriverAsciiDat::load( const std::string &datFile, MDAL::Mesh *mesh, M isVector = ( line == "VECTOR" ); group = std::make_shared< DatasetGroup >( + name(), mesh, mDatFile, - name + groupName ); group->setIsScalar( !isVector ); } @@ -114,7 +115,7 @@ void MDAL::DriverAsciiDat::load( const std::string &datFile, MDAL::Mesh *mesh, M // see if it contains face-centered results - supported by BASEMENT bool faceCentered = false; - if ( !oldFormat && contains( name, "_els_" ) ) + if ( !oldFormat && contains( groupName, "_els_" ) ) faceCentered = true; if ( group ) @@ -158,9 +159,10 @@ void MDAL::DriverAsciiDat::load( const std::string &datFile, MDAL::Mesh *mesh, M isVector = cardType == "BEGVEC"; group = std::make_shared< DatasetGroup >( + name(), mesh, mDatFile, - name + groupName ); group->setIsScalar( !isVector ); group->setIsOnVertices( !faceCentered ); diff --git a/external/mdal/frmts/mdal_binary_dat.cpp b/external/mdal/frmts/mdal_binary_dat.cpp index cc32aa0e0d85..115248c70efd 100644 --- a/external/mdal/frmts/mdal_binary_dat.cpp +++ b/external/mdal/frmts/mdal_binary_dat.cpp @@ -75,7 +75,7 @@ MDAL::DriverBinaryDat::DriverBinaryDat(): Driver( "BINARY_DAT", "Binary DAT", "*.dat", - DriverType::CanReadOnlyDatasets + Capability::ReadDatasets | Capability::WriteDatasets ) { } @@ -141,7 +141,7 @@ void MDAL::DriverBinaryDat::load( const std::string &datFile, MDAL::Mesh *mesh, int objid; int numdata; int numcells; - char name[40]; + char groupName[40]; char istat; float time; @@ -152,6 +152,7 @@ void MDAL::DriverBinaryDat::load( const std::string &datFile, MDAL::Mesh *mesh, EXIT_WITH_ERROR( MDAL_Status::Err_UnknownFormat ); std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mesh, mDatFile ); // DAT datasets @@ -160,6 +161,7 @@ void MDAL::DriverBinaryDat::load( const std::string &datFile, MDAL::Mesh *mesh, // in TUFLOW results there could be also a special timestep (99999) with maximums // we will put it into a separate dataset std::shared_ptr groupMax = std::make_shared< DatasetGroup >( + name(), mesh, mDatFile ); @@ -235,11 +237,11 @@ void MDAL::DriverBinaryDat::load( const std::string &datFile, MDAL::Mesh *mesh, case CT_NAME: // Name - if ( read( in, reinterpret_cast< char * >( &name ), 40 ) ) + if ( read( in, reinterpret_cast< char * >( &groupName ), 40 ) ) EXIT_WITH_ERROR( MDAL_Status::Err_UnknownFormat ); - if ( name[39] != 0 ) - name[39] = 0; - group->setName( trim( std::string( name ) ) ); + if ( groupName[39] != 0 ) + groupName[39] = 0; + group->setName( trim( std::string( groupName ) ) ); groupMax->setName( group->name() + "/Maximums" ); break; @@ -340,3 +342,122 @@ bool MDAL::DriverBinaryDat::readVertexTimestep( const MDAL::Mesh *mesh, } return false; //OK } + +// //////////////////////////////////////////// +// WRITE +// //////////////////////////////////////////// + +static bool writeRawData( std::ofstream &out, const char *s, int n ) +{ + out.write( s, n ); + if ( !out ) + return true; //error + else + return false; //OK +} + +bool MDAL::DriverBinaryDat::persist( MDAL::DatasetGroup *group ) +{ + std::ofstream out( group->uri(), std::ofstream::out | std::ofstream::binary ); + + // implementation based on information from: + // http://www.xmswiki.com/wiki/SMS:Binary_Dataset_Files_*.dat + if ( !out ) + return true; // Couldn't open the file + + const Mesh *mesh = group->mesh(); + size_t nodeCount = mesh->verticesCount(); + size_t elemCount = mesh->facesCount(); + + if ( !group->isOnVertices() ) + { + // Element outputs not supported in the format + return true; + } + + // version card + writeRawData( out, reinterpret_cast< const char * >( &CT_VERSION ), 4 ); + + // objecttype + writeRawData( out, reinterpret_cast< const char * >( &CT_OBJTYPE ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &CT_2D_MESHES ), 4 ); + + // float size + writeRawData( out, reinterpret_cast< const char * >( &CT_SFLT ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &CT_FLOAT_SIZE ), 4 ); + + // Flag size + writeRawData( out, reinterpret_cast< const char * >( &CT_SFLG ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &CF_FLAG_SIZE ), 4 ); + + // Dataset Group Type + if ( group->isScalar() ) + { + writeRawData( out, reinterpret_cast< const char * >( &CT_BEGSCL ), 4 ); + } + else + { + writeRawData( out, reinterpret_cast< const char * >( &CT_BEGVEC ), 4 ); + } + + // Object id (ignored) + int ignored_val = 1; + writeRawData( out, reinterpret_cast< const char * >( &CT_OBJID ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &ignored_val ), 4 ); + + // Num nodes + writeRawData( out, reinterpret_cast< const char * >( &CT_NUMDATA ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &nodeCount ), 4 ); + + // Num cells + writeRawData( out, reinterpret_cast< const char * >( &CT_NUMCELLS ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &elemCount ), 4 ); + + // Name + writeRawData( out, reinterpret_cast< const char * >( &CT_NAME ), 4 ); + writeRawData( out, MDAL::leftJustified( group->name(), 39 ).c_str(), 40 ); + + // Time steps + int istat = 1; // include if elements are active + + for ( size_t time_index = 0; time_index < group->datasets.size(); ++ time_index ) + { + const std::shared_ptr dataset = std::dynamic_pointer_cast( group->datasets[time_index] ); + + writeRawData( out, reinterpret_cast< const char * >( &CT_TS ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &istat ), 1 ); + float ftime = static_cast( dataset->time() ); + writeRawData( out, reinterpret_cast< const char * >( &ftime ), 4 ); + + if ( istat ) + { + // Write status flags + for ( size_t i = 0; i < elemCount; i++ ) + { + bool active = static_cast( dataset->active()[i] ); + writeRawData( out, reinterpret_cast< const char * >( &active ), 1 ); + } + } + + for ( size_t i = 0; i < nodeCount; i++ ) + { + // Read values flags + if ( !group->isScalar() ) + { + float x = static_cast( dataset->values()[2 * i] ); + float y = static_cast( dataset->values()[2 * i + 1 ] ); + writeRawData( out, reinterpret_cast< const char * >( &x ), 4 ); + writeRawData( out, reinterpret_cast< const char * >( &y ), 4 ); + } + else + { + float val = static_cast( dataset->values()[i] ); + writeRawData( out, reinterpret_cast< const char * >( &val ), 4 ); + } + } + } + + if ( writeRawData( out, reinterpret_cast< const char * >( &CT_ENDDS ), 4 ) ) return true; + + return false; +} diff --git a/external/mdal/frmts/mdal_binary_dat.hpp b/external/mdal/frmts/mdal_binary_dat.hpp index 3e8517035f54..fab0ba6732fa 100644 --- a/external/mdal/frmts/mdal_binary_dat.hpp +++ b/external/mdal/frmts/mdal_binary_dat.hpp @@ -29,6 +29,7 @@ namespace MDAL bool canRead( const std::string &uri ) override; void load( const std::string &datFile, Mesh *mesh, MDAL_Status *status ) override; + bool persist( DatasetGroup *group ) override; private: bool readVertexTimestep( const Mesh *mesh, diff --git a/external/mdal/frmts/mdal_cf.cpp b/external/mdal/frmts/mdal_cf.cpp index 38a486746b5e..2d1cfa0d3860 100644 --- a/external/mdal/frmts/mdal_cf.cpp +++ b/external/mdal/frmts/mdal_cf.cpp @@ -182,6 +182,7 @@ void MDAL::DriverCF::addDatasetGroups( MDAL::Mesh *mesh, const std::vector group = std::make_shared( + name(), mesh, mFileName, dsi.name @@ -245,7 +246,7 @@ void MDAL::DriverCF::parseTime( std::vector × ) MDAL::DriverCF::DriverCF( const std::string &name, const std::string &longName, const std::string &filters ): - Driver( name, longName, filters, DriverType::CanReadMeshAndDatasets ) + Driver( name, longName, filters, Capability::ReadMesh ) { } @@ -327,6 +328,7 @@ std::unique_ptr< MDAL::Mesh > MDAL::DriverCF::load( const std::string &fileName, populateFacesAndVertices( vertices, faces ); std::unique_ptr< MemoryMesh > mesh( new MemoryMesh( + name(), vertices.size(), faces.size(), mDimensions.MaxVerticesInFace, diff --git a/external/mdal/frmts/mdal_driver.cpp b/external/mdal/frmts/mdal_driver.cpp index 01127eec0c17..d717ab9a6b0b 100644 --- a/external/mdal/frmts/mdal_driver.cpp +++ b/external/mdal/frmts/mdal_driver.cpp @@ -3,17 +3,19 @@ Copyright (C) 2018 Lutra Consulting Ltd. */ +#include #include "mdal_driver.hpp" #include "mdal_utils.hpp" +#include "mdal_memory_data_model.hpp" MDAL::Driver::Driver( const std::string &name, const std::string &longName, const std::string &filters, - DriverType type ) + int capabilityFlags ) : mName( name ) , mLongName( longName ) , mFilters( filters ) - , mType( type ) + , mCapabilityFlags( capabilityFlags ) { } @@ -35,9 +37,9 @@ std::string MDAL::Driver::filters() const return mFilters; } -MDAL::DriverType MDAL::Driver::type() const +bool MDAL::Driver::hasCapability( MDAL::Capability capability ) const { - return mType; + return capability == ( mCapabilityFlags & capability ); } std::unique_ptr< MDAL::Mesh > MDAL::Driver::load( const std::string &uri, MDAL_Status *status ) @@ -54,3 +56,34 @@ void MDAL::Driver::load( const std::string &uri, Mesh *mesh, MDAL_Status *status MDAL_UNUSED( status ); return; } + +void MDAL::Driver::createDatasetGroup( MDAL::Mesh *mesh, const std::string &groupName, bool isOnVertices, bool hasScalarData, const std::string &datasetGroupFile ) +{ + std::shared_ptr grp( + new MDAL::DatasetGroup( name(), + mesh, + datasetGroupFile ) + ); + grp->setName( groupName ); + grp->setIsOnVertices( isOnVertices ); + grp->setIsScalar( hasScalarData ); + grp->startEditing(); + mesh->datasetGroups.push_back( grp ); +} + +void MDAL::Driver::createDataset( MDAL::DatasetGroup *group, double time, const double *values, const int *active ) +{ + std::shared_ptr dataset = std::make_shared< MemoryDataset >( group ); + dataset->setTime( time ); + memcpy( dataset->values(), values, sizeof( double ) * dataset->valuesCount() ); + if ( active && dataset->active() ) + memcpy( dataset->active(), active, sizeof( int ) * dataset->mesh()->facesCount() ); + dataset->setStatistics( MDAL::calculateStatistics( dataset ) ); + group->datasets.push_back( dataset ); +} + +bool MDAL::Driver::persist( MDAL::DatasetGroup *group ) +{ + MDAL_UNUSED( group ); + return true; // failure +} diff --git a/external/mdal/frmts/mdal_driver.hpp b/external/mdal/frmts/mdal_driver.hpp index 2dad1cae8ba5..564827e6b199 100644 --- a/external/mdal/frmts/mdal_driver.hpp +++ b/external/mdal/frmts/mdal_driver.hpp @@ -12,10 +12,12 @@ namespace MDAL { - enum DriverType + enum Capability { - CanReadMeshAndDatasets, - CanReadOnlyDatasets + None = 0, + ReadMesh = 1 << 0, //! Can read mesh and all datasets stored in the mesh file + ReadDatasets = 1 << 1, //! Can read only datasets (groups) from existing mesh + WriteDatasets = 1 << 2, //! Can write datasets (groups) }; class Driver @@ -24,7 +26,7 @@ namespace MDAL Driver( const std::string &name, const std::string &longName, const std::string &filters, - DriverType type + int capabilityFlags ); virtual ~Driver(); @@ -33,7 +35,7 @@ namespace MDAL std::string name() const; std::string longName() const; std::string filters() const; - DriverType type() const; + bool hasCapability( Capability capability ) const; virtual bool canRead( const std::string &uri ) = 0; @@ -42,11 +44,29 @@ namespace MDAL // loads datasets virtual void load( const std::string &uri, Mesh *mesh, MDAL_Status *status ); + // create new dataset group + virtual void createDatasetGroup( + Mesh *mesh, + const std::string &groupName, + bool isOnVertices, + bool hasScalarData, + const std::string &datasetGroupFile ); + + // create new dataset from array + virtual void createDataset( DatasetGroup *group, + double time, + const double *values, + const int *active ); + + // persist to the file + // returns true on error, false on success + virtual bool persist( DatasetGroup *group ); + private: std::string mName; std::string mLongName; std::string mFilters; - DriverType mType; + int mCapabilityFlags; }; } // namespace MDAL diff --git a/external/mdal/frmts/mdal_flo2d.cpp b/external/mdal/frmts/mdal_flo2d.cpp index cff570a58d56..d45607a7ad6b 100644 --- a/external/mdal/frmts/mdal_flo2d.cpp +++ b/external/mdal/frmts/mdal_flo2d.cpp @@ -73,13 +73,14 @@ static double getDouble( const std::string &val ) void MDAL::DriverFlo2D::addStaticDataset( bool isOnVertices, std::vector &vals, - const std::string &name, + const std::string &groupName, const std::string &datFileName ) { std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mMesh.get(), datFileName, - name + groupName ); group->setIsOnVertices( isOnVertices ); group->setIsScalar( true ); @@ -186,6 +187,7 @@ void MDAL::DriverFlo2D::parseTIMDEPFile( const std::string &datFileName, const s size_t face_idx = 0; std::shared_ptr depthDsGroup = std::make_shared< DatasetGroup >( + name(), mMesh.get(), datFileName, "Depth" @@ -195,6 +197,7 @@ void MDAL::DriverFlo2D::parseTIMDEPFile( const std::string &datFileName, const s std::shared_ptr waterLevelDsGroup = std::make_shared< DatasetGroup >( + name(), mMesh.get(), datFileName, "Water Level" @@ -203,6 +206,7 @@ void MDAL::DriverFlo2D::parseTIMDEPFile( const std::string &datFileName, const s waterLevelDsGroup->setIsScalar( true ); std::shared_ptr flowDsGroup = std::make_shared< DatasetGroup >( + name(), mMesh.get(), datFileName, "Velocity" @@ -486,6 +490,7 @@ void MDAL::DriverFlo2D::createMesh( const std::vector &cells, double mMesh.reset( new MemoryMesh( + name(), vertices.size(), faces.size(), 4, //maximum quads @@ -567,6 +572,7 @@ bool MDAL::DriverFlo2D::parseHDF5Datasets( const std::string &datFileName ) // Create dataset now std::shared_ptr ds = std::make_shared< DatasetGroup >( + name(), mMesh.get(), datFileName, grpName @@ -630,7 +636,7 @@ MDAL::DriverFlo2D::DriverFlo2D() "FLO2D", "Flo2D", "*.nc", - DriverType::CanReadMeshAndDatasets ) + Capability::ReadMesh ) { } diff --git a/external/mdal/frmts/mdal_flo2d.hpp b/external/mdal/frmts/mdal_flo2d.hpp index 441b056a7c86..43637a3a403f 100644 --- a/external/mdal/frmts/mdal_flo2d.hpp +++ b/external/mdal/frmts/mdal_flo2d.hpp @@ -47,7 +47,7 @@ namespace MDAL void parseTIMDEPFile( const std::string &datFileName, const std::vector &elevations ); void parseFPLAINFile( std::vector &elevations, const std::string &datFileName, std::vector &cells ); void parseCADPTSFile( const std::string &datFileName, std::vector &cells ); - void addStaticDataset( bool isOnVertices, std::vector &vals, const std::string &name, const std::string &datFileName ); + void addStaticDataset( bool isOnVertices, std::vector &vals, const std::string &groupName, const std::string &datFileName ); static MDAL::Vertex createVertex( size_t position, double half_cell_size, const CellCenter &cell ); static double calcCellSize( const std::vector &cells ); }; diff --git a/external/mdal/frmts/mdal_gdal.cpp b/external/mdal/frmts/mdal_gdal.cpp index 7c09801de61c..53daa1182af1 100644 --- a/external/mdal/frmts/mdal_gdal.cpp +++ b/external/mdal/frmts/mdal_gdal.cpp @@ -367,6 +367,7 @@ void MDAL::DriverGdal::addDatasetGroups() continue; std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mMesh.get(), mFileName, band->first @@ -405,6 +406,7 @@ void MDAL::DriverGdal::createMesh() initFaces( vertices, faces, is_longitude_shifted ); mMesh.reset( new MemoryMesh( + name(), vertices.size(), faces.size(), 4, //maximum quads @@ -483,7 +485,7 @@ MDAL::DriverGdal::DriverGdal( const std::string &name, const std::string &description, const std::string &filter, const std::string &gdalDriverName ): - Driver( name, description, filter, DriverType::CanReadMeshAndDatasets ), + Driver( name, description, filter, Capability::ReadMesh ), mGdalDriverName( gdalDriverName ), mPafScanline( nullptr ) {} diff --git a/external/mdal/frmts/mdal_hec2d.cpp b/external/mdal/frmts/mdal_hec2d.cpp index d4bfbdbee377..e0994c7f8d9f 100644 --- a/external/mdal/frmts/mdal_hec2d.cpp +++ b/external/mdal/frmts/mdal_hec2d.cpp @@ -119,6 +119,7 @@ void MDAL::DriverHec2D::readFaceOutput( const HdfFile &hdfFile, double eps = std::numeric_limits::min(); std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mMesh.get(), mFileName, datasetName @@ -216,6 +217,7 @@ std::shared_ptr MDAL::DriverHec2D::readElemOutput( const Hd double eps = std::numeric_limits::min(); std::shared_ptr group = std::make_shared< DatasetGroup >( + name(), mMesh.get(), mFileName, datasetName @@ -441,6 +443,7 @@ void MDAL::DriverHec2D::parseMesh( mMesh.reset( new MemoryMesh( + name(), vertices.size(), faces.size(), maxVerticesInFace, @@ -456,7 +459,7 @@ MDAL::DriverHec2D::DriverHec2D() : Driver( "HEC2D", "HEC-RAS 2D", "*.hdf", - DriverType::CanReadMeshAndDatasets ) + Capability::ReadMesh ) { } diff --git a/external/mdal/frmts/mdal_sww.cpp b/external/mdal/frmts/mdal_sww.cpp index eeca7da08657..c5e3fabd92d8 100644 --- a/external/mdal/frmts/mdal_sww.cpp +++ b/external/mdal/frmts/mdal_sww.cpp @@ -20,7 +20,7 @@ MDAL::DriverSWW::DriverSWW() : Driver( "SWW", "AnuGA", "*.sww", - DriverType::CanReadMeshAndDatasets ) + Capability::ReadMesh ) { } @@ -178,6 +178,7 @@ std::unique_ptr MDAL::DriverSWW::load( const std::string &resultsFil } std::unique_ptr< MDAL::MemoryMesh > mesh( new MemoryMesh( + name(), nodes.size(), elements.size(), 3, // triangles @@ -190,6 +191,7 @@ std::unique_ptr MDAL::DriverSWW::load( const std::string &resultsFil // Create a dataset for the bed elevation std::shared_ptr bedDs = std::make_shared ( + name(), mesh.get(), mFileName, "Bed Elevation" ); @@ -249,6 +251,7 @@ std::unique_ptr MDAL::DriverSWW::load( const std::string &resultsFil // load results std::shared_ptr dss = std::make_shared ( + name(), mesh.get(), mFileName, "Stage" ); @@ -256,6 +259,7 @@ std::unique_ptr MDAL::DriverSWW::load( const std::string &resultsFil dss->setIsScalar( true ); std::shared_ptr dsd = std::make_shared ( + name(), mesh.get(), mFileName, "Depth" ); @@ -326,6 +330,7 @@ std::unique_ptr MDAL::DriverSWW::load( const std::string &resultsFil nc_inq_varid( ncid, "ymomentum", &momentumyid ) == NC_NOERR ) { std::shared_ptr mds = std::make_shared ( + name(), mesh.get(), mFileName, "Momentum" ); diff --git a/external/mdal/frmts/mdal_xmdf.cpp b/external/mdal/frmts/mdal_xmdf.cpp index e8af487faf9a..f71d0ed57d4d 100644 --- a/external/mdal/frmts/mdal_xmdf.cpp +++ b/external/mdal/frmts/mdal_xmdf.cpp @@ -89,7 +89,7 @@ MDAL::DriverXmdf::DriverXmdf() : Driver( "XMDF", "TUFLOW XMDF", "*.xmdf", - DriverType::CanReadOnlyDatasets ) + Capability::ReadDatasets ) { } @@ -166,10 +166,10 @@ void MDAL::DriverXmdf::load( const std::string &datFile, MDAL::Mesh *mesh, MDAL HdfGroup gMaximums = gMesh.group( "Maximums" ); if ( gMaximums.isValid() ) { - for ( const std::string &name : gMaximums.groups() ) + for ( const std::string &groupName : gMaximums.groups() ) { - HdfGroup g = gMaximums.group( name ); - std::shared_ptr maxGroup = readXmdfGroupAsDatasetGroup( g, name + "/Maximums", vertexCount, faceCount ); + HdfGroup g = gMaximums.group( groupName ); + std::shared_ptr maxGroup = readXmdfGroupAsDatasetGroup( g, groupName + "/Maximums", vertexCount, faceCount ); if ( !maxGroup || maxGroup->datasets.size() != 1 ) MDAL::debug( "Maximum dataset should have just one timestep!" ); else @@ -197,10 +197,10 @@ void MDAL::DriverXmdf::load( const std::string &datFile, MDAL::Mesh *mesh, MDAL void MDAL::DriverXmdf::addDatasetGroupsFromXmdfGroup( DatasetGroups &groups, const HdfGroup &rootGroup, size_t vertexCount, size_t faceCount ) { - for ( const std::string &name : rootGroup.groups() ) + for ( const std::string &groupName : rootGroup.groups() ) { - HdfGroup g = rootGroup.group( name ); - std::shared_ptr ds = readXmdfGroupAsDatasetGroup( g, name, vertexCount, faceCount ); + HdfGroup g = rootGroup.group( groupName ); + std::shared_ptr ds = readXmdfGroupAsDatasetGroup( g, groupName, vertexCount, faceCount ); if ( ds && ds->datasets.size() > 0 ) groups.push_back( ds ); } @@ -208,7 +208,7 @@ void MDAL::DriverXmdf::addDatasetGroupsFromXmdfGroup( DatasetGroups &groups, con std::shared_ptr MDAL::DriverXmdf::readXmdfGroupAsDatasetGroup( - const HdfGroup &rootGroup, const std::string &name, size_t vertexCount, size_t faceCount ) + const HdfGroup &rootGroup, const std::string &groupName, size_t vertexCount, size_t faceCount ) { std::shared_ptr group; std::vector gDataNames = rootGroup.datasets(); @@ -218,7 +218,7 @@ std::shared_ptr MDAL::DriverXmdf::readXmdfGroupAsDatasetGrou !MDAL::contains( gDataNames, "Mins" ) || !MDAL::contains( gDataNames, "Maxs" ) ) { - MDAL::debug( "ignoring dataset " + name + " - not having required arrays" ); + MDAL::debug( "ignoring dataset " + groupName + " - not having required arrays" ); return group; } @@ -241,7 +241,7 @@ std::shared_ptr MDAL::DriverXmdf::readXmdfGroupAsDatasetGrou dimMaxs.size() != 1 ) { - MDAL::debug( "ignoring dataset " + name + " - arrays not having correct dimension counts" ); + MDAL::debug( "ignoring dataset " + groupName + " - arrays not having correct dimension counts" ); return group; } hsize_t nTimeSteps = dimTimes[0]; @@ -251,12 +251,12 @@ std::shared_ptr MDAL::DriverXmdf::readXmdfGroupAsDatasetGrou dimMins[0] != nTimeSteps || dimMaxs[0] != nTimeSteps ) { - MDAL::debug( "ignoring dataset " + name + " - arrays not having correct dimension sizes" ); + MDAL::debug( "ignoring dataset " + groupName + " - arrays not having correct dimension sizes" ); return group; } if ( dimValues[1] != vertexCount || dimActive[1] != faceCount ) { - MDAL::debug( "ignoring dataset " + name + " - not aligned with the used mesh" ); + MDAL::debug( "ignoring dataset " + groupName + " - not aligned with the used mesh" ); return group; } @@ -266,9 +266,10 @@ std::shared_ptr MDAL::DriverXmdf::readXmdfGroupAsDatasetGrou // all fine, set group and return group = std::make_shared( + name(), mMesh, mDatFile, - name + groupName ); group->setIsScalar( !isVector ); group->setIsOnVertices( true ); diff --git a/external/mdal/frmts/mdal_xmdf.hpp b/external/mdal/frmts/mdal_xmdf.hpp index 1f83a4a5b44e..d7426bcd6a4a 100644 --- a/external/mdal/frmts/mdal_xmdf.hpp +++ b/external/mdal/frmts/mdal_xmdf.hpp @@ -70,7 +70,7 @@ namespace MDAL std::string mDatFile; std::shared_ptr readXmdfGroupAsDatasetGroup( const HdfGroup &rootGroup, - const std::string &name, + const std::string &groupName, size_t vertexCount, size_t faceCount ); diff --git a/external/mdal/mdal.cpp b/external/mdal/mdal.cpp index 4a9c7dbe61d8..1a36fbdd9cf2 100644 --- a/external/mdal/mdal.cpp +++ b/external/mdal/mdal.cpp @@ -12,6 +12,7 @@ #include "mdal.h" #include "mdal_driver_manager.hpp" #include "mdal_data_model.hpp" +#include "mdal_utils.hpp" #define NODATA std::numeric_limits::quiet_NaN() @@ -21,7 +22,7 @@ static MDAL_Status sLastStatus; const char *MDAL_Version() { - return "0.1.2"; + return "0.1.3"; } MDAL_Status MDAL_LastStatus() @@ -50,6 +51,12 @@ int MDAL_driverCount() DriverH MDAL_driverFromIndex( int index ) { + if ( index < 0 ) + { + sLastStatus = MDAL_Status::Err_MissingDriver; + return nullptr; + } + size_t idx = static_cast( index ); std::shared_ptr driver = MDAL::DriverManager::instance().driver( idx ); return static_cast( driver.get() ); @@ -71,7 +78,18 @@ bool MDAL_DR_meshLoadCapability( DriverH driver ) } MDAL::Driver *d = static_cast< MDAL::Driver * >( driver ); - return d->type() == MDAL::DriverType::CanReadMeshAndDatasets; + return d->hasCapability( MDAL::Capability::ReadMesh ); +} + +bool MDAL_DR_writeDatasetsCapability( DriverH driver ) +{ + if ( !driver ) + { + sLastStatus = MDAL_Status::Err_MissingDriver; + return false; + } + MDAL::Driver *d = static_cast< MDAL::Driver * >( driver ); + return d->hasCapability( MDAL::Capability::WriteDatasets ); } const char *MDAL_DR_longName( DriverH driver ) @@ -262,6 +280,72 @@ DatasetGroupH MDAL_M_datasetGroup( MeshH mesh, int index ) return static_cast< DatasetH >( m->datasetGroups[i].get() ); } +DatasetGroupH MDAL_M_addDatasetGroup( + MeshH mesh, + const char *name, + bool isOnVertices, + bool hasScalarData, + DriverH driver, + const char *datasetGroupFile ) +{ + if ( !mesh ) + { + sLastStatus = MDAL_Status::Err_IncompatibleMesh; + return nullptr; + } + + if ( !name ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + return nullptr; + } + + if ( !datasetGroupFile ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + return nullptr; + } + + if ( !driver ) + { + sLastStatus = MDAL_Status::Err_MissingDriver; + return nullptr; + } + + MDAL::Mesh *m = static_cast< MDAL::Mesh * >( mesh ); + MDAL::Driver *dr = static_cast< MDAL::Driver * >( driver ); + + if ( !dr->hasCapability( MDAL::Capability::WriteDatasets ) ) + { + sLastStatus = MDAL_Status::Err_MissingDriverCapability; + return nullptr; + } + + const size_t index = m->datasetGroups.size(); + dr->createDatasetGroup( m, + name, + isOnVertices, + hasScalarData, + datasetGroupFile + ); + if ( index < m->datasetGroups.size() ) // we have new dataset group + return static_cast< DatasetGroupH >( m->datasetGroups[ index ].get() ); + else + return nullptr; +} + +const char *MDAL_M_driverName( MeshH mesh ) +{ + if ( !mesh ) + { + sLastStatus = MDAL_Status::Err_IncompatibleMesh; + return nullptr; + } + + MDAL::Mesh *m = static_cast< MDAL::Mesh * >( mesh ); + return _return_str( m->driverName() ); +} + /////////////////////////////////////////////////////////////////////////////////////// /// MESH VERTICES /////////////////////////////////////////////////////////////////////////////////////// @@ -506,6 +590,138 @@ void MDAL_G_minimumMaximum( DatasetGroupH group, double *min, double *max ) *max = stats.maximum; } +DatasetH MDAL_G_addDataset( DatasetGroupH group, double time, const double *values, const int *active ) +{ + if ( !group ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + return nullptr; + } + + if ( !values ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + return nullptr; + } + + MDAL::DatasetGroup *g = static_cast< MDAL::DatasetGroup * >( group ); + if ( !g->isInEditMode() ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + return nullptr; + } + + const std::string driverName = g->driverName(); + std::shared_ptr dr = MDAL::DriverManager::instance().driver( driverName ); + if ( !dr ) + { + sLastStatus = MDAL_Status::Err_MissingDriver; + return nullptr; + } + + if ( !dr->hasCapability( MDAL::Capability::WriteDatasets ) ) + { + sLastStatus = MDAL_Status::Err_MissingDriverCapability; + return nullptr; + } + + const size_t index = g->datasets.size(); + dr->createDataset( g, + time, + values, + active + ); + if ( index < g->datasets.size() ) // we have new dataset + return static_cast< DatasetGroupH >( g->datasets[ index ].get() ); + else + return nullptr; +} + +bool MDAL_G_isInEditMode( DatasetGroupH group ) +{ + if ( !group ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + return true; + } + MDAL::DatasetGroup *g = static_cast< MDAL::DatasetGroup * >( group ); + return g->isInEditMode(); +} + +void MDAL_G_closeEditMode( DatasetGroupH group ) +{ + if ( !group ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + return; + } + MDAL::DatasetGroup *g = static_cast< MDAL::DatasetGroup * >( group ); + + if ( !g->isInEditMode() ) + { + return; + } + + g->setStatistics( MDAL::calculateStatistics( g ) ); + g->stopEditing(); + + const std::string driverName = g->driverName(); + std::shared_ptr dr = MDAL::DriverManager::instance().driver( driverName ); + if ( !dr ) + { + sLastStatus = MDAL_Status::Err_MissingDriver; + return; + } + + if ( !dr->hasCapability( MDAL::Capability::WriteDatasets ) ) + { + sLastStatus = MDAL_Status::Err_MissingDriverCapability; + return; + } + + bool error = dr->persist( g ); + if ( error ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + } +} + + +void MDAL_G_setMetadata( DatasetGroupH group, const char *key, const char *val ) +{ + if ( !group ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + } + + if ( !key ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + return; + } + + if ( !val ) + { + sLastStatus = MDAL_Status::Err_InvalidData; + return; + } + + const std::string k( key ); + const std::string v( val ); + MDAL::DatasetGroup *g = static_cast< MDAL::DatasetGroup * >( group ); + g->setMetadata( k, v ); +} + +const char *MDAL_G_driverName( DatasetGroupH group ) +{ + if ( !group ) + { + sLastStatus = MDAL_Status::Err_IncompatibleDataset; + return EMPTY_STR; + } + MDAL::DatasetGroup *g = static_cast< MDAL::DatasetGroup * >( group ); + return _return_str( g->driverName() ); +} /////////////////////////////////////////////////////////////////////////////////////// /// DATASETS diff --git a/external/mdal/mdal_data_model.cpp b/external/mdal/mdal_data_model.cpp index 9a9f7a9ae709..2de652b5fd6b 100644 --- a/external/mdal/mdal_data_model.cpp +++ b/external/mdal/mdal_data_model.cpp @@ -17,6 +17,11 @@ MDAL::Dataset::Dataset( MDAL::DatasetGroup *parent ) assert( mParent ); } +std::string MDAL::Dataset::driverName() const +{ + return group()->driverName(); +} + size_t MDAL::Dataset::valuesCount() const { if ( group()->isOnVertices() ) @@ -69,20 +74,30 @@ void MDAL::Dataset::setIsValid( bool isValid ) mIsValid = isValid; } -MDAL::DatasetGroup::DatasetGroup( MDAL::Mesh *parent, +MDAL::DatasetGroup::DatasetGroup( const std::string &driverName, + MDAL::Mesh *parent, const std::string &uri, const std::string &name ) - : mParent( parent ) + : mDriverName( driverName ) + , mParent( parent ) , mUri( uri ) { assert( mParent ); setName( name ); } +std::string MDAL::DatasetGroup::driverName() const +{ + return mDriverName; +} + MDAL::DatasetGroup::~DatasetGroup() = default; -MDAL::DatasetGroup::DatasetGroup( MDAL::Mesh *parent, const std::string &uri ) - : mParent( parent ) +MDAL::DatasetGroup::DatasetGroup( const std::string &driverName, + MDAL::Mesh *parent, + const std::string &uri ) + : mDriverName( driverName ) + , mParent( parent ) , mUri( uri ) { assert( mParent ); @@ -145,6 +160,21 @@ MDAL::Mesh *MDAL::DatasetGroup::mesh() const return mParent; } +bool MDAL::DatasetGroup::isInEditMode() const +{ + return mInEditMode; +} + +void MDAL::DatasetGroup::startEditing() +{ + mInEditMode = true; +} + +void MDAL::DatasetGroup::stopEditing() +{ + mInEditMode = false; +} + bool MDAL::DatasetGroup::isOnVertices() const { return mIsOnVertices; @@ -171,8 +201,15 @@ void MDAL::DatasetGroup::setIsScalar( bool isScalar ) mIsScalar = isScalar; } -MDAL::Mesh::Mesh( size_t verticesCount, size_t facesCount, size_t faceVerticesMaximumCount, MDAL::BBox extent, const std::string &uri ) - : mVerticesCount( verticesCount ) +MDAL::Mesh::Mesh( + const std::string &driverName, + size_t verticesCount, + size_t facesCount, + size_t faceVerticesMaximumCount, + MDAL::BBox extent, + const std::string &uri ) + : mDriverName( driverName ) + , mVerticesCount( verticesCount ) , mFacesCount( facesCount ) , mFaceVerticesMaximumCount( faceVerticesMaximumCount ) , mExtent( extent ) @@ -180,6 +217,11 @@ MDAL::Mesh::Mesh( size_t verticesCount, size_t facesCount, size_t faceVerticesMa { } +std::string MDAL::Mesh::driverName() const +{ + return mDriverName; +} + MDAL::Mesh::~Mesh() = default; void MDAL::Mesh::setSourceCrs( const std::string &str ) diff --git a/external/mdal/mdal_data_model.hpp b/external/mdal/mdal_data_model.hpp index 606973147b45..ebbc4f7cbcd4 100644 --- a/external/mdal/mdal_data_model.hpp +++ b/external/mdal/mdal_data_model.hpp @@ -44,6 +44,8 @@ namespace MDAL Dataset( DatasetGroup *parent ); virtual ~Dataset(); + std::string driverName() const; + size_t valuesCount() const; virtual size_t scalarData( size_t indexStart, size_t count, double *buffer ) = 0; virtual size_t vectorData( size_t indexStart, size_t count, double *buffer ) = 0; @@ -73,15 +75,20 @@ namespace MDAL class DatasetGroup { public: - DatasetGroup( Mesh *parent, - const std::string &uri ); + DatasetGroup( const std::string &driverName, + Mesh *parent, + const std::string &uri + ); - DatasetGroup( Mesh *parent, + DatasetGroup( const std::string &driverName, + Mesh *parent, const std::string &uri, const std::string &name ); ~DatasetGroup(); + std::string driverName() const; + std::string getMetadata( const std::string &key ); void setMetadata( const std::string &key, const std::string &val ); @@ -104,7 +111,14 @@ namespace MDAL Mesh *mesh() const; + bool isInEditMode() const; + void startEditing(); + void stopEditing(); + private: + bool mInEditMode = false; + + const std::string mDriverName; Mesh *mParent = nullptr; bool mIsScalar = true; bool mIsOnVertices = true; @@ -136,13 +150,16 @@ namespace MDAL class Mesh { public: - Mesh( size_t verticesCount, + Mesh( const std::string &driverName, + size_t verticesCount, size_t facesCount, size_t faceVerticesMaximumCount, BBox extent, const std::string &uri ); virtual ~Mesh(); + std::string driverName() const; + void setSourceCrs( const std::string &str ); void setSourceCrsFromWKT( const std::string &wkt ); void setSourceCrsFromEPSG( int code ); @@ -165,6 +182,7 @@ namespace MDAL size_t faceVerticesMaximumCount() const; private: + const std::string mDriverName; size_t mVerticesCount = 0; size_t mFacesCount = 0; size_t mFaceVerticesMaximumCount = 0; //typically 3 or 4, sometimes up to 9 diff --git a/external/mdal/mdal_driver_manager.cpp b/external/mdal/mdal_driver_manager.cpp index d280e167a4ec..3a3674d0fb9b 100644 --- a/external/mdal/mdal_driver_manager.cpp +++ b/external/mdal/mdal_driver_manager.cpp @@ -41,7 +41,7 @@ std::unique_ptr MDAL::DriverManager::load( const std::string &meshFi for ( const auto &driver : mDrivers ) { - if ( ( driver->type() == DriverType::CanReadMeshAndDatasets ) && + if ( ( driver->hasCapability( Capability::ReadMesh ) ) && driver->canRead( meshFile ) ) { std::unique_ptr drv( driver->create() ); @@ -73,7 +73,7 @@ void MDAL::DriverManager::loadDatasets( Mesh *mesh, const std::string &datasetFi for ( const auto &driver : mDrivers ) { - if ( ( driver->type() == DriverType::CanReadOnlyDatasets ) && + if ( driver->hasCapability( Capability::ReadDatasets ) && driver->canRead( datasetFile ) ) { std::unique_ptr drv( driver->create() ); @@ -93,7 +93,7 @@ size_t MDAL::DriverManager::driversCount() const std::shared_ptr MDAL::DriverManager::driver( size_t index ) const { - if ( mDrivers.size() < index ) + if ( mDrivers.size() <= index ) { return std::shared_ptr(); } diff --git a/external/mdal/mdal_memory_data_model.cpp b/external/mdal/mdal_memory_data_model.cpp index 0bbbc413b4ac..9418a352c1e6 100644 --- a/external/mdal/mdal_memory_data_model.cpp +++ b/external/mdal/mdal_memory_data_model.cpp @@ -91,8 +91,18 @@ size_t MDAL::MemoryDataset::vectorData( size_t indexStart, size_t count, double return copyValues; } -MDAL::MemoryMesh::MemoryMesh( size_t verticesCount, size_t facesCount, size_t faceVerticesMaximumCount, MDAL::BBox extent, const std::string &uri ) - : MDAL::Mesh( verticesCount, facesCount, faceVerticesMaximumCount, extent, uri ) +MDAL::MemoryMesh::MemoryMesh( const std::string &driverName, + size_t verticesCount, + size_t facesCount, + size_t faceVerticesMaximumCount, + MDAL::BBox extent, + const std::string &uri ) + : MDAL::Mesh( driverName, + verticesCount, + facesCount, + faceVerticesMaximumCount, + extent, + uri ) { } diff --git a/external/mdal/mdal_memory_data_model.hpp b/external/mdal/mdal_memory_data_model.hpp index 3e168702857b..3f09aa386401 100644 --- a/external/mdal/mdal_memory_data_model.hpp +++ b/external/mdal/mdal_memory_data_model.hpp @@ -78,7 +78,8 @@ namespace MDAL class MemoryMesh: public Mesh { public: - MemoryMesh( size_t verticesCount, + MemoryMesh( const std::string &driverName, + size_t verticesCount, size_t facesCount, size_t faceVerticesMaximumCount, BBox extent, diff --git a/external/mdal/mdal_utils.cpp b/external/mdal/mdal_utils.cpp index 983fad3183f7..f7779e86d9fd 100644 --- a/external/mdal/mdal_utils.cpp +++ b/external/mdal/mdal_utils.cpp @@ -182,6 +182,21 @@ std::string MDAL::join( const std::vector parts, const std::string return res.str(); } +std::string MDAL::leftJustified( const std::string &str, size_t width, char fill ) +{ + std::string ret( str ); + if ( ret.size() > width ) + { + ret = ret.substr( 0, width ); + } + else + { + ret = ret + std::string( width - ret.size(), fill ); + } + assert( ret.size() == width ); + return ret; +} + std::string MDAL::toLower( const std::string &std ) { std::string res( std ); @@ -222,13 +237,6 @@ std::string MDAL::replace( const std::string &str, const std::string &substr, co return res; } -std::string MDAL::removeLastChar( const std::string &str ) -{ - std::string ret( str ); - ret.pop_back(); - return ret; -} - MDAL::BBox MDAL::computeExtent( const MDAL::Vertices &vertices ) { BBox b; @@ -348,7 +356,12 @@ MDAL::Statistics _calculateStatistics( const std::vector &values, size_t return ret; } -MDAL::Statistics MDAL::calculateStatistics( std::shared_ptr grp ) +MDAL::Statistics MDAL::calculateStatistics( std::shared_ptr grp ) +{ + return calculateStatistics( grp.get() ); +} + +MDAL::Statistics MDAL::calculateStatistics( DatasetGroup *grp ) { Statistics ret; if ( !grp ) @@ -416,6 +429,7 @@ void MDAL::addBedElevationDatasetGroup( MDAL::Mesh *mesh, const Vertices &vertic return; std::shared_ptr group = std::make_shared< DatasetGroup >( + mesh->driverName(), mesh, mesh->uri(), "Bed Elevation" diff --git a/external/mdal/mdal_utils.hpp b/external/mdal/mdal_utils.hpp index 6387336f231d..2f93aaea665d 100644 --- a/external/mdal/mdal_utils.hpp +++ b/external/mdal/mdal_utils.hpp @@ -47,7 +47,8 @@ namespace MDAL bool contains( const std::string &str, const std::string &substr, ContainsBehaviour behaviour = CaseSensitive ); bool contains( const std::vector &list, const std::string &str ); std::string replace( const std::string &str, const std::string &substr, const std::string &replacestr, ContainsBehaviour behaviour = CaseSensitive ); - std::string removeLastChar( const std::string &str ); + //! left justify and truncate, resulting string will always have width chars + std::string leftJustified( const std::string &str, size_t width, char fill = ' ' ); std::string toLower( const std::string &std ); @@ -102,6 +103,7 @@ namespace MDAL //! Calculates statistics for dataset group Statistics calculateStatistics( std::shared_ptr grp ); + Statistics calculateStatistics( DatasetGroup *grp ); //! Calculates statistics for dataset Statistics calculateStatistics( std::shared_ptr dataset ); From 08206980f7633d02513f33582488310985c9f6a5 Mon Sep 17 00:00:00 2001 From: Peter Petrik Date: Tue, 18 Dec 2018 14:25:08 +0100 Subject: [PATCH 2/2] [feature] [mesh] Mesh Calculator Similarly to raster calculator, mesh calculator can take dataset groups from current mesh layer and combine them with various aritmentic/logical operators to new dataset group. --- images/images.qrc | 1 + .../default/mActionShowMeshCalculator.png | Bin 0 -> 6536 bytes python/CMakeLists.txt | 2 + python/analysis/analysis_auto.sip | 1 + .../mesh/qgsmeshcalculator.sip.in | 109 ++ .../mesh/qgsmeshdataprovider.sip.in | 23 + .../auto_generated/mesh/qgsmeshlayer.sip.in | 2 + src/analysis/CMakeLists.txt | 14 + src/analysis/mesh/qgsmeshcalclexer.ll | 92 ++ src/analysis/mesh/qgsmeshcalcnode.cpp | 234 ++++ src/analysis/mesh/qgsmeshcalcnode.h | 164 +++ src/analysis/mesh/qgsmeshcalcparser.yy | 168 +++ src/analysis/mesh/qgsmeshcalculator.cpp | 194 +++ src/analysis/mesh/qgsmeshcalculator.h | 126 ++ src/analysis/mesh/qgsmeshcalcutils.cpp | 1124 +++++++++++++++++ src/analysis/mesh/qgsmeshcalcutils.h | 281 +++++ src/app/CMakeLists.txt | 5 +- src/app/mesh/qgsmeshcalculatordialog.cpp | 557 ++++++++ src/app/mesh/qgsmeshcalculatordialog.h | 129 ++ .../qgsmeshrendereractivedatasetwidget.cpp | 18 +- .../mesh/qgsmeshrendereractivedatasetwidget.h | 8 +- src/app/qgisapp.cpp | 71 ++ src/app/qgisapp.h | 2 + src/core/mesh/qgsmeshdataprovider.h | 22 + src/core/mesh/qgsmeshlayer.cpp | 9 + src/core/mesh/qgsmeshlayer.h | 6 + src/core/mesh/qgsmeshmemorydataprovider.cpp | 190 ++- src/core/mesh/qgsmeshmemorydataprovider.h | 36 +- src/core/mesh/qgstriangularmesh.h | 4 +- src/providers/mdal/qgsmdalprovider.cpp | 70 + src/providers/mdal/qgsmdalprovider.h | 8 + src/ui/mesh/qgsmeshcalculatordialogbase.ui | 649 ++++++++++ src/ui/qgisapp.ui | 51 +- src/ui/qgsrastercalcdialogbase.ui | 6 +- tests/src/analysis/CMakeLists.txt | 6 +- tests/src/analysis/testqgsmeshcalculator.cpp | 218 ++++ tests/src/app/CMakeLists.txt | 7 +- tests/src/app/testqgsmeshcalculatordialog.cpp | 109 ++ .../mesh/quad_and_triangle_vertex_scalar2.dat | 21 + .../quad_and_triangle_vertex_scalar_max.dat | 15 + .../mesh/quad_and_triangle_vertex_vector2.dat | 21 + .../quad_and_triangle_vertex_vector_max.dat | 15 + 42 files changed, 4720 insertions(+), 68 deletions(-) create mode 100644 images/themes/default/mActionShowMeshCalculator.png create mode 100644 python/analysis/auto_generated/mesh/qgsmeshcalculator.sip.in create mode 100644 src/analysis/mesh/qgsmeshcalclexer.ll create mode 100644 src/analysis/mesh/qgsmeshcalcnode.cpp create mode 100644 src/analysis/mesh/qgsmeshcalcnode.h create mode 100644 src/analysis/mesh/qgsmeshcalcparser.yy create mode 100644 src/analysis/mesh/qgsmeshcalculator.cpp create mode 100644 src/analysis/mesh/qgsmeshcalculator.h create mode 100644 src/analysis/mesh/qgsmeshcalcutils.cpp create mode 100644 src/analysis/mesh/qgsmeshcalcutils.h create mode 100644 src/app/mesh/qgsmeshcalculatordialog.cpp create mode 100644 src/app/mesh/qgsmeshcalculatordialog.h create mode 100644 src/ui/mesh/qgsmeshcalculatordialogbase.ui create mode 100644 tests/src/analysis/testqgsmeshcalculator.cpp create mode 100644 tests/src/app/testqgsmeshcalculatordialog.cpp create mode 100644 tests/testdata/mesh/quad_and_triangle_vertex_scalar2.dat create mode 100644 tests/testdata/mesh/quad_and_triangle_vertex_scalar_max.dat create mode 100644 tests/testdata/mesh/quad_and_triangle_vertex_vector2.dat create mode 100644 tests/testdata/mesh/quad_and_triangle_vertex_vector_max.dat diff --git a/images/images.qrc b/images/images.qrc index 91ff9ad5396d..9eb041c1d7dc 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -375,6 +375,7 @@ themes/default/mActionShowPinnedLabels.svg themes/default/mActionShowPluginManager.svg themes/default/mActionShowRasterCalculator.png + themes/default/mActionShowMeshCalculator.png themes/default/mActionShowSelectedLayers.svg themes/default/mActionSimplify.svg themes/default/mActionSplitFeatures.svg diff --git a/images/themes/default/mActionShowMeshCalculator.png b/images/themes/default/mActionShowMeshCalculator.png new file mode 100644 index 0000000000000000000000000000000000000000..8099150603d18aff6fa9357404b746056463f1b7 GIT binary patch literal 6536 zcmV;38F%K1P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*ta^$+Qr2k_Ty#!(gmV?pk?Vy+6p97Ris#2YE z@3U4`i4>EGAqEfkV4C%R|7V;3!%wWvnGkbHHKm82P(yW%lk(i3d$dnDzwaL(@7eFy z&F34AOP-^@f2Q@`-?$!seISSTdjEdiUpTSKfiVyVvdex2f;v zTKKY^_y7J_ZN7)!{|*^r%*c4&8MnOk55JRU@vOjC-_B9Lozq$5Jo?1%;Ooz}GoPow z8SnG#H`_h!z3#u8!aV7{A^m2f_qvTg+wNHCo7aBL{Oyfr*z|n;`D(j8%iinmu7!l9 zl6g1vv6XYijU7|^^RdJ$^FMN)&#Uq(Y;oYRi8nj1VQjSyYi}Fuw9Rh2&T-hm5`$YV zb98Pvf36juU3KF)Bj{zf%eZG6U+~qyYo5#Qb1nAFThDxhpq^4Uo9vUF%hqDYf`1w-cRsj|2W*UPTC?$u z_X+0|r@8q|w&z|K-MtL7;0UEi#x*1m&I%zCKRFa)s6`)xi7}>_V~I7kUKo_g-3 z*WL!iWa*~~KQY|Ab}X@wP6T6vXKSKGwe_B-sj)6To>y4$DM zUax-ln*Ggl|LisUdQBb+=f3>QYh2FxV+boaiSij9bHU>AEDsRSPCk2z4Z+EC^4U`z zNf8}hvr*2ORvyEHVObZq{q)^0&;6s{Obq{(-`w9kXXLv7AD%OE-RrqO{q}>`W;+$d z9|tK5Wz)xRfNj?1N^`vjFz=j}zaG8KBkaB9IHBb8tlpo!!ZdNznPUaObh$pPdG9z& z-Yzv3_`_hUjXQNIYkyod|F(udh8Zd8q^;@8bFMX&SMzc5{Wu5!PYf13u9dbm{H{J` zmr00RD%c;0BfU3?vOP>~ow)dOsEyC=%+B}m`@4T0|FWWO|K@=aX7aK0GW}Z9xdNq0 z(OXl}=*5MVUC3>B_(hu2;sQ42!p`ON*h@7<^iz z5kD(GXYm+)l998Vjbo!_Q?eoIUw*p(7AEUIjt=vmg$bTL@xYc#z)V}t+_jVJRrV~^ zoG=eA5)u>4*<&_kLi*9qVYJwvv-nWnZD8SviH**)W2YaN|J!3OV{&3_oDFcivv9?dKe)tu97pERkRVc(uN5Co5fhhI-bNSzyot%gFKB#)j3RouC(i0-E!WVokWj z^z;IUUu)#LYngy;sSWbPtTp*w3HwTV%n0rm(_>tH_L{~_z)PQH5Qm z3>Jx1LRZCRz{|$&9!w?z*na7z0KbE=ZDzYMwe3Srm}@r>>`8)>8d=g#6c$26v78}< zt~XGv1DJ&@9@fm3LT`E1owxwDyD$=y5L3OA6^9Ibp+T$RF!T8;;?%8wgdr@g9 z8gXYZVg@QyAk8!N#@A${^HeEdg>vEl0<&iFD>CJMw791ruXu9G2=3-d?C>hXUGifHwaE+R+ct0;d2i=U&D2 zEFk4={yzWklkF}?j8iTmaXDiMFr|mWf_;I4neh0xaug11QD#P6B}07@5S+>!u09Ns z@?ifBhD?t6Am^3lXM^CZv7Bx-ANN{j4OW*SM_gIW)ePFNIg0BM>O!32T9U`D9Fn(D zk=$iyY#&X_tPu({!TpA6O>6tu`^u0as1^l-WrAa{p#Yt6&_IAF^=5CJr`V=!YGy1l zo(d9cY(!H&7>K-`GHY(0=z_jQ(h*wt1g0d0S_471gXBEtep7-n9(S~H=Ac$_XwBwM zI^z!lk@&%Qvs)bVx31=PD$Gs=ZK3t4?CKd`yx z0wH%GILj6x07Nb;>?Tqw*V<7B7skURfsM-1DM#tQ;K0gNbRWXFh>iL zrd}s8(ShPqmqzI^F?ZZ#n{abx7#Fq+cpL7N9|1%(fQAn}6QEgDgMhT_Rt-O+KUL#q zAPcLLl>;z6k;i!5J$-c?n9FhoSOT&Tb;!!@Y%LqA4;3ydl_JaXdj*{p`ryW`ZN=_W z+)k37OXi0n>{Kh`$ry^toN4JJ5kPeNnG@V=Sxd({4L*zueNQIUg6M!ZAkDc@Jd}Q< zKCLhVyOpysK-9+*czs_A3a}CK*n}cBxXVEE)^+1Ul;Dsp5Nu81eku7b z)X$Bvcv+X}NRvo;I>Y%*JneUh8m=@U8vwm1;2t#0MP6HI9*5OfVW7TAD?yhMsX^K% zVt5~_3baFDh!aXjn_#UYpTJXmx~w&q2{m98CiI+b6qFTmDc4Ruv-&9EPU$ad~x~t~v4%Bq!c0uxJuh zZhMMeU6nMCj^!jORa41Y5p%$}$ncCTbY3LHS5S~_8Wnw7fGDPD%^V2`sQJly;OeU! zhNm)I_F5rQ>~1mWh~xrJ5D2b`Mg{9BO5rU>ex(3eMV3-dJ9$L@A5HfvI&wIIVy#dx zP`HG7x1m*aYNmkh0iqtfY#{JSe2kSqePIh^k8NmECH`rfxq(*GU` zLL>nr6Qa{0BLQL<;ZiVJ0Kb61;62rkq}?9{N&+hrK`KxXALwvdg%}^*AW2!wLjETU zafa`ma@bnCz0z*sb9f;xYU;BNEw@U{2JP7*y`VBiGU7UJk$~I=+b$3xq6NvwQW2Yp zrIsX$S)Mkbu&Yf#t3%XabqR`^aj}iw_(BAu(x^D9w8>JKdf>mv8TFjetp{I7yvsee zI`JnU(t=@v^UlJ=ZqgFSM;@&4aSK#kO1QItF!=PE4VIeur&g%+uFG$Z&ZZnSyD8q9 z$cz&SZEGO|+y+Bu&SEl-a_b$Dq#mnUzA4CT&58{OS&!ii0-{`2cq4c|mXbV9vxpK| zaXtx>`Z}8`jw`5TLYz=l1NLJt=5sSuCwTU4qCm%55OA;RZ(52yBg51z2AdX0$U@Oz zgkUGvfT(qe$wLu=T?_nYyPS1TjlvipHl^$+uI~oD=0v!fOwotGY)KKIiqQ*0BtADq z-Y8Z1UE0T>;!C91Q5vUE^>AqoFeE=fv8n8g?T@DbeB@i!hKBuDrcHsIpKP5l_{P_X z7curuRj4q*U;16RLyZ)B=S?yV`PyZh%z^Z4SprwFq%V7RLs`KU9lR2O9ETq^WAJCo(PGov;RM zR9lj6O;PPpJ@26ku~zsd$`p=2ym&8^!z@BM4+s0lBv!(QWmMo#g26^4M1P2Ju~kB& zmK!Ln4A%ZIn)lcRK4gNPepnGpfSWQ48&T8fQKu3Ufa_tRPrU#sL7ZBVKf0P{6W5Os zZR(GYCV6rj7}CO6!UMR7aZqa&JE@Z zt70^Vck6lPV?YJy@TrV=*5cXx=~+en@hy%OeWGAkLJ1jtQam4ETW3YEz|%>+uJ)_rxxN%r^J6=+26d9)IQom? zl`v_8zCwWlwQ8MC=SDP)RL_&EjEy78zIMFVcSH3e zFJ0TGIs^XS-)iIzp&wWNMNSiZNa9+OF0_t%>sdNh!qD5>)i$D%#9t<7N|aiJX?U#% zlM??4sYl5V<1mPvAGOthuk4|L@$gPLMUC;Pefi~|OG^Q;FKs#exnzF0hmiu>?(`QQy8tovbm3Zx zjlVv+J3T|b%N}-aa~dn_8tlqrHCHkMWikrkM?Ogm+%}Ya7}AaXn|-^VaxzS~s(NEvO(@PKfS52Q4Ca*!8U2ZSZJ{c&o57V%CWE81>SM zdAr>gc~z#vOMPDSeR3dotH`bKaM8@vK3;NrI8{32Rz@Wltf**L8})Qif!L^rZ}V*@ zBZ124XP@f*2TQmd;e#Q}e`x0^W?=^ApQ*ujW*|;LxjTO~n-$P6^;ksq4d}}~@@Kd3 zuSt>g7^Plu<`5sU&Ws_7r>BjuS^YNY~*X6 z;#^4${j6s%lmIMRRU`VV{QG6p(9$RGkGb#6Do2BXdV{~#P01{*yW)JUw-<=dC8gAz zDyiPeXK55G%3?)K9afzpMTQ~-Z?GY|mldhG@u=qR+s^~_Db4v+S1lgObrk@Rw7i~s zhUEkwKZfDDY1%XSo4Ym)M9x4GOQ># zOwT;G*5@!)(zbe!Z~vw~5aAU) z#eSpU9(6Du;-*xuM+JZ@7U)1}QwNzcIr}6dgIU(~i+_g%ycA%o89-`1dI(PSg26(L zd6Pn_4hA5UcY3D2+pq(GPe4lQJY}oBLqITSiJ_NP5WgjfG@!U4;t8)pwXlTGtX3;t z5P$%0tn4ot$dd~NAh)|kec?*dbs(4;K}CFlq+>=hOSv#@C!EmVTTfcSPO951UL@M6 z(d!b#BMa*@e{51JWXB*V$~zlBR=nLd*=xT^^&cez>rS^FFgL7iQnqOgif*6Eq-Z9<^r)G4z+ZRk36D z6(@Yy>w6}yp&N{Ah-XeoCQ2%9eE z?u5ZfROoH2Bqj_KcDvzALHf)D_~p?tYP6r8@cUfN9vuS%vCn<}zf#R_Blx8fM9wi6 zQ3D=bIph1MoEcYad~|L;nbH^ErC$w&a{6i_qh6xQ>MM#e0pL(V&NELx>*@Vbr^rP+ zOHT*eNl6?E=fE9BHk?i5+T0p*C1>1s6;zp^=G_r@ed2T9WU z%JrzuA_*nE`UHw`n+t4klY3P!N^5$&j<^Aj(8)xWO*gWl0)%4HqaJ4Jj;wF|5mRda zw{co@N+x`oXM+l;rTZ{O`s}k`_4a8{bBX0E-=2w>k&;Edoo5W{0bm+f;lm3lt}j6K zVqt@7PO;?=$!LDijAXT};pboV!q;Q337xVbqiK7XPP4&MY4l|}O0$cj4Hr%&Ur+15 z^!HYh{-A!%7x{Y_l$wDQCRS5Xqn>kplaPu5PVdBek$$ms^+-q;0;+>ySV`YN0O^Ky zpq4~b`09Dt+prLB0;g5wUPR3wjGtb8<(TzYlQpZ>!Ptsw!@m6!nA1DM#{{j{LpjDE z@l`{AdmsHa^Z?7;#{nN*nBxBJ1K;n*B=7C&vHkrMca^{H{)b!5Z=*xN!OvN#f?sSR zvwik?WVX2VH9MHkX;gi#Xt{q}5#ku`L8UBzSy^jy7yy|3d0@MPV(!OuP%QC6pJ0tG zOlA0&uZsQj^)L7>CHthmh^;6lzu(k%!kM>U;-~_O{oNG%y{fs>sL&|%)IuN|>@@0J zAki{lu_Jb>=j(&)1VEk?R}?sewy%5|34bqs|Hb#%fXHlXu7TRSyS{jG9wnUzNel#+ z(32VulV~ZyWs>M(Q^(qhTtQf?`G1S3w0_#Vi+lh800v@9M??Vs0RI60puMM)00009 za7bBm000fw000fw0YWI7cmMzZ2XskIMF-;n0uDGahqAML000A*Nkld3fXh@|S(7pcZf5~rXAZ(R7m0`ffSKvh@$t2#H*RcxwV}ZpY;NYh z!NI9V=H_xyJZX7D=STp&o>yi?K}X7(<&ek9Lj*3biLM^mz-e$xzUM<@JM&J zHQwL9=rX2}foDc~dUjtM8nQ^r>yF1$4P!n8z-blO&H|)~^u-S9@tBQjtL>IEhZKNZ1{KEWk($2><~hLh?4(h?}84glCAq&V;0;r*bzqC8__Vgdu-4~)hBid`+-TDZ&ioKHr=k;KiP zZ-(@&9$U7=%K*Mi8#HDn05C92Al6z#Ykk{21~c=&V}M~&-V=b+o`Dd8j4?lUws(FK zI1w1`>gqZsgqYRPdrh7hotd3!4Tr{8&4lh;lQR937DCdt|eM)TTcNvwG8sg zDz60HGeJpJ%~Q6^{?ySt7L6y4^oNhnBx85J^-i8kWPU6y+7SOccKBkX;qpCM#LN%- z{r+d&*@fTuN_{z#lT&8o*I%A2D=Rycd`{l%g1W~yKk@wf_3OmBb7xDHQsn^hR#gCC zyTfSETCd$%Tuh^*qvGoDl@hJ>(WGR9fUQN_iey1S0ebuTatw*j1e=X3=0GV`4dAhk zj*gla_I=dIY~D1+AfffXOQFvr$=H%N-ml2WkgY-pKv3J+rlb3nQf&ZwRtj3JF{YS^ zz5sxj*;!gxS+>44xzwry(43M$DU~aw?6)jyLw &datasetValues, + const QVector &datasetActive, + const QVector × + ) = 0; +%Docstring +Creates a new dataset group from a data and +persists it into a destination path + +On success, the mesh's dataset group count is changed + +:param path: destination path of the stored file +:param meta: new group's metadata +:param datasetValues: scalar/vector values for all datasets and all faces/vertices in the group +:param datasetActive: active flag values for all datasets in the group. Empty array represents can be used + when all faces are active + +:return: true on failure, false on success + .. versionadded:: 3.6 %End }; diff --git a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in index ecb3e5514df0..8252ef57acfb 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -130,6 +130,8 @@ Returns the provider type for this layer + + QgsMeshRendererSettings rendererSettings() const; %Docstring Returns renderer settings diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 6937ae26fac9..04e2f1bbdcf4 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -134,6 +134,10 @@ SET(QGIS_ANALYSIS_SRCS vector/qgsgeometrysnappersinglesource.cpp vector/qgszonalstatistics.cpp + mesh/qgsmeshcalcnode.cpp + mesh/qgsmeshcalculator.cpp + mesh/qgsmeshcalcutils.cpp + network/qgsgraph.cpp network/qgsgraphbuilder.cpp network/qgsgraphbuilderinterface.cpp @@ -201,14 +205,18 @@ FIND_PACKAGE(EXIV2 REQUIRED) INCLUDE_DIRECTORIES(SYSTEM ${SPATIALITE_INCLUDE_DIR}) INCLUDE_DIRECTORIES(SYSTEM ${SQLITE3_INCLUDE_DIR}) INCLUDE_DIRECTORIES(BEFORE raster) +INCLUDE_DIRECTORIES(BEFORE mesh) ADD_FLEX_FILES_PREFIX(QGIS_ANALYSIS_SRCS raster raster/qgsrastercalclexer.ll) +ADD_FLEX_FILES_PREFIX(QGIS_ANALYSIS_SRCS mesh mesh/qgsmeshcalclexer.ll) ADD_BISON_FILES_PREFIX(QGIS_ANALYSIS_SRCS raster raster/qgsrastercalcparser.yy) +ADD_BISON_FILES_PREFIX(QGIS_ANALYSIS_SRCS mesh mesh/qgsmeshcalcparser.yy) IF(NOT MSVC) SET_SOURCE_FILES_PROPERTIES( ${CMAKE_BINARY_DIR}/src/analysis/qgsrastercalcparser.cpp + ${CMAKE_BINARY_DIR}/src/analysis/qgsmeshcalcparser.cpp PROPERTIES COMPILE_FLAGS "-w" ) ELSE(NOT MSVC) @@ -217,6 +225,7 @@ ELSE(NOT MSVC) # 4702 unreachable code SET_SOURCE_FILES_PROPERTIES( ${CMAKE_BINARY_DIR}/src/analysis/qgsrastercalcparser.cpp + ${CMAKE_BINARY_DIR}/src/analysis/qgsmeshcalcparser.cpp PROPERTIES COMPILE_FLAGS "-wd4127 -wd4702" ) ENDIF(PEDANTIC) @@ -255,6 +264,10 @@ SET(QGIS_ANALYSIS_HDRS raster/qgsrastermatrix.h raster/qgsrastercalcnode.h raster/qgstotalcurvaturefilter.h + + mesh/qgsmeshcalcnode.h + mesh/qgsmeshcalculator.h + mesh/qgsmeshcalcutils.h vector/mersenne-twister.h vector/qgsgeometrysnapper.h @@ -326,6 +339,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/geometry ${CMAKE_SOURCE_DIR}/src/core/processing ${CMAKE_SOURCE_DIR}/src/core/raster + ${CMAKE_SOURCE_DIR}/src/core/mesh ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/metadata ${CMAKE_SOURCE_DIR}/src/core/expression diff --git a/src/analysis/mesh/qgsmeshcalclexer.ll b/src/analysis/mesh/qgsmeshcalclexer.ll new file mode 100644 index 000000000000..7f4133226c13 --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalclexer.ll @@ -0,0 +1,92 @@ +/*************************************************************************** + qgsmeshcalclexer.ll + ------------------- + begin : December 19th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +%option noyywrap +%option nounput +%option case-insensitive +%option never-interactive + + // ensure that lexer will be 8-bit (and not just 7-bit) +%option 8bit + +%{ + //directly included in the output program + #include "qgsmeshcalcnode.h" + #include "qgsmeshcalcparser.hpp" + + // if not defined, searches for isatty() + // which doesn't in MSVC compiler + #define YY_NEVER_INTERACTIVE 1 + + #ifdef _MSC_VER + #define YY_NO_UNISTD_H + #endif + + #ifndef _MSC_VER + #pragma GCC diagnostic ignored "-Wsign-compare" + #endif +%} + +white [ \t\r\n]+ + +dig [0-9] +num1 {dig}+\.?([eE][-+]?{dig}+)? +num2 {dig}*\.{dig}+([eE][-+]?{dig}+)? +number {num1}|{num2} + +non_ascii [\x80-\xFF] +dataset_ref_char [A-Za-z0-9_./:]|{non_ascii}|[-] +dataset_ref ({dataset_ref_char}+) +dataset_ref_quoted \"(\\.|[^"])*\" + +%% + +"sum_aggr" { meshlval.op = QgsMeshCalcNode::opSUM_AGGR; return FUNCTION; } +"max_aggr" { meshlval.op = QgsMeshCalcNode::opMAX_AGGR; return FUNCTION; } +"min_aggr" { meshlval.op = QgsMeshCalcNode::opMIN_AGGR; return FUNCTION; } +"average_aggr" { meshlval.op = QgsMeshCalcNode::opAVG_AGGR; return FUNCTION; } +"abs" { meshlval.op = QgsMeshCalcNode::opABS; return FUNCTION; } + +"max" { meshlval.op = QgsMeshCalcNode::opMAX; return FUNCTION2; } +"min" { meshlval.op = QgsMeshCalcNode::opMIN; return FUNCTION2; } + +"IF" { return IF; } +"AND" { return AND; } +"OR" { return OR; } +"NOT" { return NOT; } +"!=" { return NE; } +"<=" { return LE; } +">=" { return GE; } +"NODATA" {return NODATA;} + +[=><+-/*^] { return yytext[0]; } + +[()] { return yytext[0]; } + +{number} { meshlval.number = atof(meshtext); return NUMBER; } + +{dataset_ref} { return DATASET_REF; } + +{dataset_ref_quoted} { return DATASET_REF; } + +{white} /* skip blanks and tabs */ +%% + +void set_mesh_input_buffer(const char* buffer) +{ + mesh_scan_string(buffer); +} diff --git a/src/analysis/mesh/qgsmeshcalcnode.cpp b/src/analysis/mesh/qgsmeshcalcnode.cpp new file mode 100644 index 000000000000..e3ba6fd87eea --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalcnode.cpp @@ -0,0 +1,234 @@ +/*************************************************************************** + qgsmeshcalcnode.cpp + ------------------- + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +///@cond PRIVATE + +#include + +#include "qgsmeshcalcnode.h" +#include "qgsmeshmemorydataprovider.h" + +QgsMeshCalcNode::QgsMeshCalcNode() + : mType( tNoData ) +{ +} + +QgsMeshCalcNode::QgsMeshCalcNode( double number ) + : mType( tNumber ) + , mNumber( number ) +{ +} + + +QgsMeshCalcNode::QgsMeshCalcNode( Operator op, QgsMeshCalcNode *left, QgsMeshCalcNode *right ) + : mType( tOperator ) + , mLeft( left ) + , mRight( right ) + , mOperator( op ) +{ +} + +QgsMeshCalcNode::QgsMeshCalcNode( QgsMeshCalcNode *condition /* bool condition */, + QgsMeshCalcNode *left /*if true */, + QgsMeshCalcNode *right /* if false */ ) + : mType( tOperator ) + , mLeft( left ) + , mRight( right ) + , mCondition( condition ) + , mOperator( opIF ) +{ +} + +QgsMeshCalcNode::QgsMeshCalcNode( const QString &datasetGroupName ) + : mType( tDatasetGroupRef ) + , mDatasetGroupName( datasetGroupName ) +{ + if ( mDatasetGroupName.startsWith( '"' ) && mDatasetGroupName.endsWith( '"' ) ) + mDatasetGroupName = mDatasetGroupName.mid( 1, mDatasetGroupName.size() - 2 ); +} + +QgsMeshCalcNode::~QgsMeshCalcNode() = default; + +QgsMeshCalcNode::Type QgsMeshCalcNode::type() const +{ + return mType; +} + +void QgsMeshCalcNode::setLeft( QgsMeshCalcNode *left ) +{ + mLeft.reset( left ); +} + +void QgsMeshCalcNode::setRight( QgsMeshCalcNode *right ) +{ + mRight.reset( right ); +} + +QStringList QgsMeshCalcNode::usedDatasetGroupNames() const +{ + QStringList res; + + if ( mType == tDatasetGroupRef ) + { + res.append( mDatasetGroupName ); + } + + if ( mLeft ) + { + res += mLeft->usedDatasetGroupNames(); + } + + if ( mRight ) + { + res += mRight->usedDatasetGroupNames(); + } + + if ( mCondition ) + { + res += mCondition->usedDatasetGroupNames(); + } + + return res; +} + +bool QgsMeshCalcNode::calculate( const QgsMeshCalcUtils &dsu, QgsMeshMemoryDatasetGroup &result ) const +{ + if ( mType == tDatasetGroupRef ) + { + dsu.copy( result, mDatasetGroupName ); + return true; + } + else if ( mType == tOperator ) + { + QgsMeshMemoryDatasetGroup leftDatasetGroup( "left" ); + QgsMeshMemoryDatasetGroup rightDatasetGroup( "right" ); + + if ( !mLeft || !mLeft->calculate( dsu, leftDatasetGroup ) ) + { + return false; + } + if ( mRight && !mRight->calculate( dsu, rightDatasetGroup ) ) + { + return false; + } + + QgsMeshMemoryDatasetGroup condition( "condition" ); + switch ( mOperator ) + { + case opIF: + // Evaluate boolean condition + if ( !mCondition->calculate( dsu, condition ) ) + { + // invalid boolean condition + return false; + } + dsu.addIf( leftDatasetGroup, rightDatasetGroup, condition ); + break; + + case opPLUS: + dsu.add( leftDatasetGroup, rightDatasetGroup ); + break; + case opMINUS: + dsu.subtract( leftDatasetGroup, rightDatasetGroup ); + break; + case opMUL: + dsu.multiply( leftDatasetGroup, rightDatasetGroup ); + break; + case opDIV: + dsu.divide( leftDatasetGroup, rightDatasetGroup ); + break; + case opPOW: + dsu.power( leftDatasetGroup, rightDatasetGroup ); + break; + case opEQ: + dsu.equal( leftDatasetGroup, rightDatasetGroup ); + break; + case opNE: + dsu.notEqual( leftDatasetGroup, rightDatasetGroup ); + break; + case opGT: + dsu.greaterThan( leftDatasetGroup, rightDatasetGroup ); + break; + case opLT: + dsu.lesserThan( leftDatasetGroup, rightDatasetGroup ); + break; + case opGE: + dsu.greaterEqual( leftDatasetGroup, rightDatasetGroup ); + break; + case opLE: + dsu.lesserEqual( leftDatasetGroup, rightDatasetGroup ); + break; + case opAND: + dsu.logicalAnd( leftDatasetGroup, rightDatasetGroup ); + break; + case opOR: + dsu.logicalOr( leftDatasetGroup, rightDatasetGroup ); + break; + case opNOT: + dsu.logicalNot( leftDatasetGroup ); + break; + case opMIN: + dsu.minimum( leftDatasetGroup, rightDatasetGroup ); + break; + case opMAX: + dsu.maximum( leftDatasetGroup, rightDatasetGroup ); + break; + case opABS: + dsu.abs( leftDatasetGroup ); + break; + case opSUM_AGGR: + dsu.sumAggregated( leftDatasetGroup ); + break; + case opMIN_AGGR: + dsu.minimumAggregated( leftDatasetGroup ); + break; + case opMAX_AGGR: + dsu.maximumAggregated( leftDatasetGroup ); + break; + case opAVG_AGGR: + dsu.averageAggregated( leftDatasetGroup ); + break; + case opSIGN: + dsu.changeSign( leftDatasetGroup ); + break; + default: + return false; + } + dsu.tranferDatasets( result, leftDatasetGroup ); + return true; + } + else if ( mType == tNumber ) + { + dsu.number( result, mNumber ); + return true; + } + else if ( mType == tNoData ) + { + dsu.nodata( result ); + return true; + } + + // invalid type + return false; +} + +QgsMeshCalcNode *QgsMeshCalcNode::parseMeshCalcString( const QString &str, QString &parserErrorMsg ) +{ + extern QgsMeshCalcNode *localParseMeshCalcString( const QString & str, QString & parserErrorMsg ); + return localParseMeshCalcString( str, parserErrorMsg ); +} + +///@endcond diff --git a/src/analysis/mesh/qgsmeshcalcnode.h b/src/analysis/mesh/qgsmeshcalcnode.h new file mode 100644 index 000000000000..607c573aeafb --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalcnode.h @@ -0,0 +1,164 @@ +/*************************************************************************** + qgsmeshcalcnode.h + -------------------------------- + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSMESHCALCNODE_H +#define QGSMESHCALCNODE_H + +#define SIP_NO_FILE + +///@cond PRIVATE + +#include +#include +#include +#include +#include + +#include "qgis_analysis.h" + +#include "qgsmeshcalcutils.h" + +struct QgsMeshMemoryDatasetGroup; + +/** + * \ingroup analysis + * \class QgsMeshCalcNode + * Represents a single calculation node + * + * \since QGIS 3.6 + */ +class ANALYSIS_EXPORT QgsMeshCalcNode +{ + public: + //! types of mesh node + enum Type + { + tOperator = 1, //!< Operator (e.g. +, -) + tNumber, //!< Number (e.g. 1) + tNoData, //!< Nodata (NaN) + tDatasetGroupRef //!< Dataset group + }; + + //! operators between dataset groups + enum Operator + { + opPLUS, //!< Plus + opMINUS, //!< Minus + opMUL, //!< Multiply + opDIV, //!< Divide + opPOW, //!< Power + opEQ, //!< Equal + opNE, //!< Not equal + opGT, //!< Greater than + opLT, //!< Lower than + opGE, //!< Greater or equal + opLE, //!< Lower or equal + opAND, //!< Boolean AND + opOR, //!< Boolean OR + opNOT, //!< Boolean NOT (unary) + opIF, //!< Boolean IF + opSIGN, //!< Change sign (unary) + opMIN, //!< Minimum value + opMAX, //!< Maximum value + opABS, //!< Absolute value (unary) + opSUM_AGGR, //!< Aggregated sum (unary) + opMAX_AGGR, //!< Aggregated maximum (unary) + opMIN_AGGR, //!< Aggregated minimum (unary) + opAVG_AGGR, //!< Aggregated average (unary) + opNONE, //!< Nodata value + }; + + /** + * Constructs a Type::tNoData node initialized with NODATA values + */ + QgsMeshCalcNode(); + + /** + * Constructs a Type::tNumber node initialized with selected number + */ + QgsMeshCalcNode( double number ); + + /** + * Constructs a Type::tOperator node for binary or unary operator + * \param op Operator to perform + * \param left Left node. This node takes ownership of the node + * \param right Right node (for binary operators). This node takes ownership of the node + */ + QgsMeshCalcNode( Operator op, QgsMeshCalcNode *left, QgsMeshCalcNode *right ); + + /** + * Constructs a Type::tOperator node for IF operator (condition) + * \param condition node to determine if left or right value is used + * \param left Left node. This node takes ownership of the node + * \param right Right node. This node takes ownership of the node + */ + QgsMeshCalcNode( QgsMeshCalcNode *condition /* bool condition */, + QgsMeshCalcNode *left /*if true */, + QgsMeshCalcNode *right /* if false */ ); + + /** + * Constructs a Type::tDatasetGroupRef node with values from dataset group + * \param datasetName dataset group to fetch data and populate node data + */ + QgsMeshCalcNode( const QString &datasetGroupName ); + + //! Destructor + ~QgsMeshCalcNode(); + + //! Returns type of node + Type type() const; + + //! Sets left node. This node takes ownership of the node + void setLeft( QgsMeshCalcNode *left ); + + //! Sets right node. This node takes ownership of the node + void setRight( QgsMeshCalcNode *right ); + + /** + * Calculates result of mesh calculation + * \param dsu utils with initial conditions for calculation (times, dataset groups) + * \param result destination dataset group for calculation results + * \returns true on success, false on failure + */ + bool calculate( const QgsMeshCalcUtils &dsu, QgsMeshMemoryDatasetGroup &result ) const; + + //! Returns all dataset group names used in formula + QStringList usedDatasetGroupNames() const; + + /** + * Parses string to calculation node. Caller takes responsibility to delete the node + * \param str string with formula definition + * \param parserErrorMsg error message on error + * \returns calculation node. Nullptr on error + */ + static QgsMeshCalcNode *parseMeshCalcString( const QString &str, QString &parserErrorMsg ); + + private: + Q_DISABLE_COPY( QgsMeshCalcNode ) + + Type mType = tNoData; + std::unique_ptr mLeft; + std::unique_ptr mRight; + std::unique_ptr mCondition; + double mNumber = std::numeric_limits::quiet_NaN(); + QString mDatasetGroupName; + Operator mOperator = opNONE; +}; + +///@endcond + +#endif // QGSMESHCALCNODE_H diff --git a/src/analysis/mesh/qgsmeshcalcparser.yy b/src/analysis/mesh/qgsmeshcalcparser.yy new file mode 100644 index 000000000000..d47ada25b0d7 --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalcparser.yy @@ -0,0 +1,168 @@ +/*************************************************************************** + qgsmeshcalcparser.yy + -------------------- + begin : December 19th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +%{ + #include "qgsmeshcalcnode.h" + +#ifdef _MSC_VER +# pragma warning( disable: 4065 ) // switch statement contains 'default' but no 'case' labels +# pragma warning( disable: 4701 ) // Potentially uninitialized local variable 'name' used +#endif + + // don't redeclare malloc/free + #define YYINCLUDED_STDLIB_H 1 + + QgsMeshCalcNode* parseMeshCalcString(const QString& str, QString& parserErrorMsg); + + //! from lex.yy.c + extern int meshlex(); + extern char* meshtext; + extern void set_mesh_input_buffer(const char* buffer); + + //! variable where the parser error will be stored + QString rMeshParserErrorMsg; + + //! sets gParserErrorMsg + void mesherror(const char* msg); + + //! temporary list for nodes without parent (if parsing fails these nodes are removed) + QList gMeshTmpNodes; + void joinTmpNodes(QgsMeshCalcNode* parent, QgsMeshCalcNode* left, QgsMeshCalcNode* right, QgsMeshCalcNode* condition); + void addToTmpNodes(QgsMeshCalcNode* node); + + // we want verbose error messages + #define YYERROR_VERBOSE 1 +%} + +%union { QgsMeshCalcNode* node; double number; QgsMeshCalcNode::Operator op;} + +%start root + +%token NODATA +%token DATASET_REF +%token NUMBER +%token FUNCTION +%token FUNCTION2 + +%type root +%type mesh_exp + +%left AND +%left OR +%left NOT +%left NE +%left GE +%left LE +%left IF + +%left '=' '<' '>' +%left '+' '-' +%left '*' '/' +%left '^' +%left UMINUS // fictitious symbol (for unary minus) + +%% + +root: mesh_exp{} +; + +mesh_exp: + FUNCTION '(' mesh_exp ')' { $$ = new QgsMeshCalcNode($1, $3, 0); joinTmpNodes($$, $3, 0, 0);} + | FUNCTION2 '(' mesh_exp ',' mesh_exp ')' { $$ = new QgsMeshCalcNode($1, $3, $5); joinTmpNodes($$, $3, $5, 0);} + | IF '(' mesh_exp ',' mesh_exp ',' mesh_exp ')' { $$ = new QgsMeshCalcNode($3, $5, $7); joinTmpNodes($$, $3, $5, $7);} + | NOT '(' mesh_exp ')' { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opNOT, $3, 0 ); joinTmpNodes($$,$3, 0, 0); } + | mesh_exp AND mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opAND, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp OR mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opOR, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '=' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opEQ, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp NE mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opNE, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '>' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opGT, $1, $3 ); joinTmpNodes($$, $1, $3, 0); } + | mesh_exp '<' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opLT, $1, $3 ); joinTmpNodes($$, $1, $3, 0); } + | mesh_exp GE mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opGE, $1, $3 ); joinTmpNodes($$, $1, $3, 0); } + | mesh_exp LE mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opLE, $1, $3 ); joinTmpNodes($$, $1, $3, 0); } + | mesh_exp '^' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opPOW, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '*' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opMUL, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '/' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opDIV, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '+' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opPLUS, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | mesh_exp '-' mesh_exp { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opMINUS, $1, $3 ); joinTmpNodes($$,$1,$3, 0); } + | '(' mesh_exp ')' { $$ = $2; } + | '+' mesh_exp %prec UMINUS { $$ = $2; } + | '-' mesh_exp %prec UMINUS { $$ = new QgsMeshCalcNode( QgsMeshCalcNode::opSIGN, $2, 0 ); joinTmpNodes($$, $2, 0, 0); } + | NUMBER { $$ = new QgsMeshCalcNode($1); addToTmpNodes($$); } + | DATASET_REF { $$ = new QgsMeshCalcNode(QString::fromUtf8(meshtext)); addToTmpNodes($$); } + | NODATA { $$ = new QgsMeshCalcNode(); addToTmpNodes($$); } +; + +%% + +void addToTmpNodes(QgsMeshCalcNode* node) +{ + gMeshTmpNodes.append(node); +} + + +void removeTmpNode(QgsMeshCalcNode* node) +{ + bool res; + Q_UNUSED(res); + + if (node) + { + res = gMeshTmpNodes.removeAll(node) != 0; + Q_ASSERT(res); + } +} + +void joinTmpNodes(QgsMeshCalcNode* parent, QgsMeshCalcNode* left, QgsMeshCalcNode* right, QgsMeshCalcNode* condition) +{ + removeTmpNode(right); + removeTmpNode(left); + removeTmpNode(condition); + gMeshTmpNodes.append(parent); +} + + +QgsMeshCalcNode* localParseMeshCalcString(const QString& str, QString& parserErrorMsg) +{ + // list should be empty when starting + Q_ASSERT(gMeshTmpNodes.count() == 0); + + set_mesh_input_buffer(str.toUtf8().constData()); + int res = meshparse(); + + // list should be empty when parsing was OK + if (res == 0) // success? + { + Q_ASSERT(gMeshTmpNodes.count() == 1); + return gMeshTmpNodes.takeFirst(); + } + else // error? + { + parserErrorMsg = rMeshParserErrorMsg; + // remove nodes without parents - to prevent memory leaks + while (gMeshTmpNodes.size() > 0) + delete gMeshTmpNodes.takeFirst(); + return nullptr; + } +} + +void mesherror(const char* msg) +{ + rMeshParserErrorMsg = msg; +} + + + diff --git a/src/analysis/mesh/qgsmeshcalculator.cpp b/src/analysis/mesh/qgsmeshcalculator.cpp new file mode 100644 index 000000000000..3ffb62e3100e --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalculator.cpp @@ -0,0 +1,194 @@ +/*************************************************************************** + qgsmeshcalculator.cpp + --------------------- + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include +#include + +#include "qgsmeshcalcnode.h" +#include "qgsmeshcalculator.h" +#include "qgsmeshcalcutils.h" +#include "qgsmeshmemorydataprovider.h" + +QgsMeshCalculator::QgsMeshCalculator( const QString &formulaString, const QString &outputFile, + const QgsRectangle &outputExtent, double startTime, double endTime, + QgsMeshLayer *layer ) + : mFormulaString( formulaString ) + , mOutputFile( outputFile ) + , mOutputExtent( outputExtent ) + , mUseMask( false ) + , mStartTime( startTime ) + , mEndTime( endTime ) + , mMeshLayer( layer ) +{ +} + +QgsMeshCalculator::QgsMeshCalculator( const QString &formulaString, + const QString &outputFile, + const QgsGeometry &outputMask, + double startTime, + double endTime, + QgsMeshLayer *layer ) + : mFormulaString( formulaString ) + , mOutputFile( outputFile ) + , mOutputMask( outputMask ) + , mUseMask( true ) + , mStartTime( startTime ) + , mEndTime( endTime ) + , mMeshLayer( layer ) +{ +} + +QgsMeshCalculator::Result QgsMeshCalculator::expression_valid( const QString &formulaString, + QgsMeshLayer *layer ) +{ + QString errorString; + std::unique_ptr< QgsMeshCalcNode > calcNode( QgsMeshCalcNode::parseMeshCalcString( formulaString, errorString ) ); + if ( !calcNode ) + { + return ParserError; + } + + double startTime = -std::numeric_limits::max(); + double endTime = std::numeric_limits::max(); + QgsMeshCalcUtils dsu( layer, calcNode->usedDatasetGroupNames(), startTime, endTime ); + if ( !dsu.isValid() ) + { + return InvalidDatasets; + } + + return Success; +} + +QgsMeshCalculator::Result QgsMeshCalculator::processCalculation( QgsFeedback *feedback ) +{ + Q_UNUSED( feedback ); + + // check input + if ( mOutputFile.isEmpty() ) + { + return CreateOutputError; + } + + if ( !mMeshLayer || + !mMeshLayer->dataProvider() || + mMeshLayer->dataProvider()->name() != QStringLiteral( "mdal" ) + ) + { + return CreateOutputError; + } + + //prepare search string / tree + QString errorString; + std::unique_ptr< QgsMeshCalcNode > calcNode( QgsMeshCalcNode::parseMeshCalcString( mFormulaString, errorString ) ); + if ( !calcNode ) + { + return ParserError; + } + + QgsMeshCalcUtils dsu( mMeshLayer, calcNode->usedDatasetGroupNames(), mStartTime, mEndTime ); + if ( !dsu.isValid() ) + { + return InvalidDatasets; + } + + //open output dataset + std::unique_ptr outputGroup( new QgsMeshMemoryDatasetGroup( mOutputFile ) ); + + // calculate + bool ok = calcNode->calculate( dsu, *outputGroup ); + if ( !ok ) + { + return EvaluateError; + } + + if ( feedback && feedback->isCanceled() ) + { + return Canceled; + } + if ( feedback ) + { + feedback->setProgress( 60.0 ); + } + + // Finalize dataset + if ( mUseMask ) + { + dsu.filter( *outputGroup, mOutputMask ); + } + else + { + dsu.filter( *outputGroup, mOutputExtent ); + } + outputGroup->isScalar = true; + outputGroup->name = QFileInfo( mOutputFile ).baseName(); + + // before storing the file, find out if the process is not already canceled + if ( feedback && feedback->isCanceled() ) + { + return Canceled; + } + if ( feedback ) + { + feedback->setProgress( 80.0 ); + } + + // store to file + QVector datasetValues; + QVector datasetActive; + QVector times; + + for ( int i = 0; i < outputGroup->datasets.size(); ++i ) + { + const std::shared_ptr dataset = outputGroup->datasets.at( i ); + + times.push_back( dataset->time ); + datasetValues.push_back( + dataset->datasetValues( outputGroup->isScalar, + 0, + dataset->values.size() ) + ); + if ( !dataset->active.isEmpty() ) + { + datasetActive.push_back( + dataset->areFacesActive( + 0, + dataset->active.size() ) + ); + } + } + + const QgsMeshDatasetGroupMetadata meta = outputGroup->groupMetadata(); + bool err = mMeshLayer->dataProvider()->persistDatasetGroup( + mOutputFile, + meta, + datasetValues, + datasetActive, + times + ); + + if ( err ) + { + return CreateOutputError; + } + + if ( feedback ) + { + feedback->setProgress( 100.0 ); + } + return Success; +} diff --git a/src/analysis/mesh/qgsmeshcalculator.h b/src/analysis/mesh/qgsmeshcalculator.h new file mode 100644 index 000000000000..77cb71f36d9a --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalculator.h @@ -0,0 +1,126 @@ +/*************************************************************************** + qgsmeshcalculator.h + ------------------- + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSMESHCALCULATOR_H +#define QGSMESHCALCULATOR_H + +#include +#include + +#include "qgis_analysis.h" +#include "qgsrectangle.h" +#include "qgsmeshlayer.h" +#include "qgsmeshdataprovider.h" +#include "qgsfeedback.h" + +struct QgsMeshMemoryDatasetGroup; +struct QgsMeshMemoryDataset; + +/** + * \ingroup analysis + * \class QgsMeshCalculator + * Performs mesh layer calculations. + * + * Mesh calculator can do various mathematical operations + * between dataset groups from a single mesh layer. + * Resulting dataset group is added to the mesh layer. + * Result can be filtered by extent or a vector layer mask + * spatially and by selection of times. + * + * Note: only dataset groups defined on vertices are + * implemented and supported + * + * \since QGIS 3.6 +*/ +class ANALYSIS_EXPORT QgsMeshCalculator +{ + public: + + //! Result of the calculation + enum Result + { + Success = 0, //!< Calculation successful + Canceled, //!< Calculation canceled + CreateOutputError, //!< Error creating output data file + InputLayerError, //!< Error reading input layer + ParserError, //!< Error parsing formula + InvalidDatasets, //!< Datasets with different time outputs or not part of the mesh + EvaluateError, //!< Error during evaluation + MemoryError, //!< Error allocating memory for result + }; + + /** + * Creates calculator with bounding box (rectangular) mask + * \param formulaString formula/expression to evaluate. Consists of dataset group names, operators and numbers + * \param outputFile file to store the resulting dataset group data + * \param outputExtent spatial filter defined by rectangle + * \param startTime time filter defining the starting dataset + * \param endTime time filter defining the ending dataset + * \param layer mesh layer with dataset groups references in formulaString + */ + QgsMeshCalculator( const QString &formulaString, + const QString &outputFile, + const QgsRectangle &outputExtent, + double startTime, + double endTime, + QgsMeshLayer *layer ); + + /** + * Creates calculator with geometry mask + * \param formulaString formula/expression to evaluate. Consists of dataset group names, operators and numbers + * \param outputFile file to store the resulting dataset group data + * \param outputMask spatial filter defined by geometry + * \param startTime time filter defining the starting dataset + * \param endTime time filter defining the ending dataset + * \param layer mesh layer with dataset groups references in formulaString + */ + QgsMeshCalculator( const QString &formulaString, + const QString &outputFile, + const QgsGeometry &outputMask, + double startTime, + double endTime, + QgsMeshLayer *layer ); + + /** + * Starts the calculation, writes new dataset group to file and adds it to the mesh layer + * \param feedback The optional feedback argument for progress reporting and cancelation support + * \returns QgsMeshCalculator::Success in case of success + */ + Result processCalculation( QgsFeedback *feedback = nullptr ); + + /** + * Returns whether formula is valid for particular mesh layer + * \param formulaString formula/expression to evaluate. Consists of dataset group names, operators and numbers + * \param layer mesh layer with dataset groups references in formulaString + * \returns QgsMeshCalculator::Success in case of success + */ + static Result expression_valid( const QString &formulaString, QgsMeshLayer *layer ); + + private: + QgsMeshCalculator(); + + QString mFormulaString; + QString mOutputFile; + QgsRectangle mOutputExtent; + QgsGeometry mOutputMask; + bool mUseMask = false; + double mStartTime = 0.0; + double mEndTime = 0.0; + QgsMeshLayer *mMeshLayer = nullptr; +}; + +#endif // QGSMESHCALCULATOR_H diff --git a/src/analysis/mesh/qgsmeshcalcutils.cpp b/src/analysis/mesh/qgsmeshcalcutils.cpp new file mode 100644 index 000000000000..229b72e6d34e --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalcutils.cpp @@ -0,0 +1,1124 @@ +/*************************************************************************** + qgsmeshcalcutils.cpp + -------------------- + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +///@cond PRIVATE + +#include + +#include "qgsmeshcalcnode.h" +#include "qgsmeshcalcutils.h" +#include "qgsmeshmemorydataprovider.h" +#include "qgstriangularmesh.h" +#include "qgsmapsettings.h" + +static inline bool isNoData( double val ) +{ + return std::isnan( val ); +} + +static inline bool equals( double val0, double val1, double eps = std::numeric_limits::epsilon() ) +{ + return fabs( val0 - val1 ) < eps; +} + +const double D_TRUE = 1.0; +const double D_FALSE = 0.0; +const double D_NODATA = std::numeric_limits::quiet_NaN(); + +std::shared_ptr QgsMeshCalcUtils::create( const QString &datasetGroupName ) const +{ + const auto dp = mMeshLayer->dataProvider(); + std::shared_ptr grp; + for ( int group_i = 0; group_i < dp->datasetGroupCount(); ++group_i ) + { + const auto meta = dp->datasetGroupMetadata( group_i ); + const QString name = meta.name(); + if ( name == datasetGroupName ) + { + grp = std::make_shared(); + grp->isScalar = meta.isScalar(); + grp->type = meta.dataType(); + grp->maximum = meta.maximum(); + grp->minimum = meta.minimum(); + grp->name = meta.name(); + + int count = ( meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnFaces ) ? dp->faceCount() : dp->vertexCount(); + for ( int dataset_i = 0; dataset_i < dp->datasetCount( group_i ); ++dataset_i ) + { + const QgsMeshDatasetIndex index( group_i, dataset_i ); + const auto dsMeta = dp->datasetMetadata( index ); + std::shared_ptr ds = create( grp->type ); + ds->maximum = dsMeta.maximum(); + ds->minimum = dsMeta.minimum(); + ds->time = dsMeta.time(); + ds->valid = dsMeta.isValid(); + + const QgsMeshDataBlock block = dp->datasetValues( index, 0, count ); + Q_ASSERT( block.count() == count ); + for ( int value_i = 0; value_i < count; ++value_i ) + ds->values[value_i] = block.value( value_i ); + + const QgsMeshDataBlock active = dp->areFacesActive( index, 0, dp->faceCount() ); + Q_ASSERT( active.count() == dp->faceCount() ); + for ( int value_i = 0; value_i < dp->faceCount(); ++value_i ) + ds->active[value_i] = active.active( value_i ); + + grp->addDataset( ds ); + } + + break; + } + } + return grp; +} + +std::shared_ptr QgsMeshCalcUtils::create( const QgsMeshMemoryDatasetGroup &grp ) const +{ + return create( grp.type ); +} + +std::shared_ptr QgsMeshCalcUtils::create( const QgsMeshDatasetGroupMetadata::DataType type ) const +{ + std::shared_ptr ds = std::make_shared(); + bool onVertices = type == QgsMeshDatasetGroupMetadata::DataOnVertices; + if ( onVertices ) + { + ds->values.resize( mMeshLayer->dataProvider()->vertexCount() ); + ds->active.resize( mMeshLayer->dataProvider()->faceCount() ); + memset( ds->active.data(), 1, static_cast( ds->active.size() ) * sizeof( int ) ); + } + else + { + ds->values.resize( mMeshLayer->dataProvider()->faceCount() ); + } + ds->valid = true; + return ds; +} + +QgsMeshCalcUtils:: QgsMeshCalcUtils( QgsMeshLayer *layer, + const QStringList &usedGroupNames, + double startTime, + double endTime ) + : mMeshLayer( layer ) + , mIsValid( false ) + , mOutputType( QgsMeshDatasetGroupMetadata::DataType::DataOnVertices ) +{ + // First populate group's names map and see if we have all groups present + // And basically fetch all data from any mesh provider to memory + for ( const QString &groupName : usedGroupNames ) + { + std::shared_ptr ds = create( groupName ); + if ( !ds ) + return; + + mDatasetGroupMap.insert( groupName, ds ); + } + + // Now populate used times and check that all datasets do have some times + // OR just one time (== one output) + bool timesPopulated = false; + const auto vals = mDatasetGroupMap.values(); + for ( const auto &ds : vals ) + { + if ( ds->datasetCount() == 0 ) + { + // dataset must have at least 1 output + return; + } + + if ( ds->datasetCount() > 1 ) + { + if ( timesPopulated ) + { + if ( ds->datasetCount() != mTimes.size() ) + { + // different number of datasets in the groupss + return; + } + } + + for ( int datasetIndex = 0; datasetIndex < ds->datasetCount(); ++datasetIndex ) + { + std::shared_ptr o = ds->constDataset( datasetIndex ); + if ( timesPopulated ) + { + if ( !equals( mTimes[datasetIndex], o->time, 1e-5 ) ) + { + // error, the times in different datasets differ + return; + } + } + else + { + mTimes.append( o->time ); + } + } + + timesPopulated = true; + } + } + + // case of all group are not time varying or usedGroupNames is empty + if ( mTimes.isEmpty() ) + { + mTimes.push_back( 0.0 ); + } + else + { + // filter out times we do not need to speed up calculations + for ( QVector::iterator it = mTimes.begin(); it != mTimes.end(); ) + { + if ( equals( *it, startTime ) || + equals( *it, endTime ) || + ( ( *it >= startTime ) && ( *it <= endTime ) ) ) + ++it; + else + it = mTimes.erase( it ); + } + } + + // check that all datasets are of the same type + for ( const auto &ds : vals ) + { + if ( ds->type != mOutputType ) + return; + } + + // All is valid! + mIsValid = true; +} + +bool QgsMeshCalcUtils::isValid() const +{ + return mIsValid; +} + +const QgsMeshLayer *QgsMeshCalcUtils::layer() const +{ + return mMeshLayer; +} + +std::shared_ptr QgsMeshCalcUtils::group( const QString &datasetName ) const +{ + return mDatasetGroupMap[datasetName]; +} + +void QgsMeshCalcUtils::populateSpatialFilter( QgsMeshMemoryDatasetGroup &filter, const QgsRectangle &extent ) const +{ + filter.clearDatasets(); + + std::shared_ptr output = create( filter ); + output->time = mTimes[0]; + + const QList faceIndexesForRectangle = triangularMesh()->faceIndexesForRectangle( extent ); + const QVector trianglesToNativeFaces = triangularMesh()->trianglesToNativeFaces(); + + if ( mOutputType == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + for ( const int faceIndex : faceIndexesForRectangle ) + { + const int nativeIndex = trianglesToNativeFaces[faceIndex]; + const QgsMeshFace face = nativeMesh()->face( nativeIndex ); + for ( const int vertexIndex : face ) + { + output->values[vertexIndex].set( D_TRUE ); + } + } + } + else + { + for ( const int faceIndex : faceIndexesForRectangle ) + { + const int nativeIndex = trianglesToNativeFaces[faceIndex]; + output->values[nativeIndex].set( D_TRUE ); + } + } + filter.addDataset( output ); +} + + +void QgsMeshCalcUtils::populateMaskFilter( QgsMeshMemoryDatasetGroup &filter, const QgsGeometry &mask ) const +{ + filter.clearDatasets(); + std::shared_ptr output = create( filter ); + output->time = mTimes[0]; + + const QVector trianglesToNativeFaces = triangularMesh()->trianglesToNativeFaces(); + const QVector &vertices = triangularMesh()->vertices(); + + if ( mOutputType == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + int nativeVertexCount = mMeshLayer->dataProvider()->vertexCount(); + + for ( int i = 0; i < nativeVertexCount; ++i ) + { + const QgsPointXY point( vertices[i] ); + if ( mask.contains( &point ) ) + { + output->values[i].set( D_TRUE ); + } + else + { + output->values[i].set( D_FALSE ); + } + } + } + else + { + const QVector &triangles = triangularMesh()->triangles(); + for ( int i = 0; i < triangles.size(); ++i ) + { + const QgsMeshFace face = triangles[i]; + const QgsGeometry geom = QgsMeshUtils::toGeometry( face, vertices ); + const QgsRectangle bbox = geom.boundingBox(); + if ( mask.intersects( bbox ) ) + { + output->values[i].set( D_TRUE ); + } + else + { + output->values[i].set( D_FALSE ); + } + } + } + filter.addDataset( output ); +} + +std::shared_ptr QgsMeshCalcUtils::number( double val, double time ) const +{ + Q_ASSERT( isValid() ); + + std::shared_ptr output = create( mOutputType ); + output->time = time; + + // by default it is initialized to 1 + if ( isNoData( val ) ) + { + if ( mOutputType == QgsMeshDatasetGroupMetadata::DataOnVertices ) + memset( output->active.data(), 0, static_cast( output->active.size() ) * sizeof( int ) ); + } + else + { + for ( int i = 0; i < output->values.size(); ++i ) // Using for loop we are initializing + { + output->values[i].set( val ); + } + } + + return output; +} + +void QgsMeshCalcUtils::number( QgsMeshMemoryDatasetGroup &group1, double val ) const +{ + Q_ASSERT( isValid() ); + + group1.datasets.clear(); + std::shared_ptr output = number( val, mTimes[0] ); + group1.datasets.push_back( output ); +} + + +void QgsMeshCalcUtils::ones( QgsMeshMemoryDatasetGroup &group1 ) const +{ + Q_ASSERT( isValid() ); + number( group1, 1.0 ); +} + +void QgsMeshCalcUtils::nodata( QgsMeshMemoryDatasetGroup &group1 ) const +{ + Q_ASSERT( isValid() ); + number( group1, D_NODATA ); +} + + +std::shared_ptr QgsMeshCalcUtils::copy( + std::shared_ptr dataset0 +) const +{ + Q_ASSERT( isValid() ); + Q_ASSERT( dataset0 ); + + std::shared_ptr output = std::make_shared(); + output->values = dataset0->values; //deep copy + output->active = dataset0->active; //deep copy + output->time = dataset0->time; + output->valid = dataset0->valid; + return output; +} + +void QgsMeshCalcUtils::copy( QgsMeshMemoryDatasetGroup &group1, const QString &groupName ) const +{ + Q_ASSERT( isValid() ); + + std::shared_ptr group2 = group( groupName ); + Q_ASSERT( group2 ); + + if ( group2->datasetCount() == 1 ) + { + // Always copy + std::shared_ptr o0 = group2->constDataset( 0 ); + std::shared_ptr output = copy( o0 ); + group1.addDataset( output ); + } + else + { + for ( int output_index = 0; output_index < group2->datasetCount(); ++output_index ) + { + std::shared_ptr o0 = group2->constDataset( output_index ); + if ( equals( o0->time, mTimes.first() ) || + equals( o0->time, mTimes.last() ) || + ( ( o0->time >= mTimes.first() ) && ( o0->time <= mTimes.last() ) ) + ) + { + std::shared_ptr output = copy( o0 ); + group1.addDataset( output ); + } + } + } +} + +void QgsMeshCalcUtils::tranferDatasets( QgsMeshMemoryDatasetGroup &group1, QgsMeshMemoryDatasetGroup &group2 ) const +{ + Q_ASSERT( isValid() ); + + group1.clearDatasets(); + for ( int i = 0; i < group2.datasetCount(); ++i ) + { + std::shared_ptr o = group2.datasets[i]; + Q_ASSERT( o ); + group1.addDataset( o ); + } + group2.clearDatasets(); +} + +void QgsMeshCalcUtils::expand( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + Q_ASSERT( isValid() ); + + if ( group2.datasetCount() > 1 ) + { + if ( group1.datasetCount() == 1 ) + { + const std::shared_ptr o0 = group1.datasets[0]; + Q_ASSERT( o0 ); + for ( int i = 1; i < group2.datasetCount(); ++i ) + { + std::shared_ptr o = copy( o0 ); + o->time = mTimes[i]; + group1.addDataset( o ); + } + } + } +} + + +std::shared_ptr QgsMeshCalcUtils::canditateDataset( + QgsMeshMemoryDatasetGroup &group, + int datasetIndex ) const +{ + Q_ASSERT( isValid() ); + + if ( group.datasetCount() > 1 ) + { + Q_ASSERT( group.datasetCount() > datasetIndex ); + return group.datasets[datasetIndex]; + } + else + { + Q_ASSERT( group.datasetCount() == 1 ); + return group.datasets[0]; + } +} + +std::shared_ptr QgsMeshCalcUtils::constCandidateDataset( + const QgsMeshMemoryDatasetGroup &group, + int datasetIndex ) const +{ + Q_ASSERT( isValid() ); + + if ( group.datasetCount() > 1 ) + { + Q_ASSERT( group.datasetCount() > datasetIndex ); + return group.constDataset( datasetIndex ); + } + else + { + Q_ASSERT( group.datasetCount() == 1 ); + return group.constDataset( 0 ); + } +} + +int QgsMeshCalcUtils::datasetCount( + const QgsMeshMemoryDatasetGroup &group1, + const QgsMeshMemoryDatasetGroup &group2 ) const +{ + Q_ASSERT( isValid() ); + + if ( ( group1.datasetCount() > 1 ) || ( group2.datasetCount() > 1 ) ) + { + return mTimes.size(); + } + else + { + return 1; + } +} + +void QgsMeshCalcUtils::func1( QgsMeshMemoryDatasetGroup &group, + std::function func ) const +{ + Q_ASSERT( isValid() ); + + for ( int time_index = 0; time_index < group.datasetCount(); ++time_index ) + { + std::shared_ptr output = canditateDataset( group, time_index ); + + for ( int n = 0; n < output->values.size(); ++n ) + { + double val1 = output->values[n].scalar(); + double res_val = D_NODATA; + if ( !isNoData( val1 ) ) + res_val = func( val1 ); + output->values[n] = res_val; + } + + if ( group.type == QgsMeshDatasetGroupMetadata::DataOnVertices ) + activate( output ); + } +} + + +void QgsMeshCalcUtils::func2( QgsMeshMemoryDatasetGroup &group1, + const QgsMeshMemoryDatasetGroup &group2, + std::function func ) const +{ + Q_ASSERT( isValid() ); + Q_ASSERT( group1.type == group2.type ); // we do not support mixed output types + + expand( group1, group2 ); + + for ( int time_index = 0; time_index < datasetCount( group1, group2 ); ++time_index ) + { + std::shared_ptr o1 = canditateDataset( group1, time_index ); + std::shared_ptr o2 = constCandidateDataset( group2, time_index ); + + for ( int n = 0; n < o2->values.size(); ++n ) + { + double val1 = o1->values[n].scalar(); + double val2 = o2->values[n].scalar(); + double res_val = D_NODATA; + if ( !isNoData( val1 ) && !isNoData( val2 ) ) + res_val = func( val1, val2 ); + o1->values[n] = res_val; + } + + if ( group1.type == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + activate( o1, o2 ); + } + + } +} + +void QgsMeshCalcUtils::funcAggr( + QgsMeshMemoryDatasetGroup &group1, + std::function& )> func +) const +{ + Q_ASSERT( isValid() ); + + if ( group1.type == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + std::shared_ptr output = QgsMeshCalcUtils::create( QgsMeshDatasetGroupMetadata::DataOnVertices ); + output->time = mTimes[0]; + for ( int n = 0; n < mMeshLayer->dataProvider()->vertexCount(); ++n ) + { + QVector < double > vals; + for ( int datasetIndex = 0; datasetIndex < group1.datasetCount(); ++datasetIndex ) + { + const std::shared_ptr o1 = canditateDataset( group1, datasetIndex ); + + double val1 = o1->values[n].scalar(); + // ideally we should take only values from cells that are active. + // but the problem is that the node can be part of multiple cells, + // few active and few not, ... + if ( !isNoData( val1 ) ) + { + vals.push_back( val1 ); + } + } + + double res_val = D_NODATA; + if ( !vals.isEmpty() ) + { + res_val = func( vals ); + } + + output->values[n] = res_val; + } + + // lets do activation purely on NODATA values as we did aggregation here + activate( output ); + + group1.datasets.clear(); + group1.datasets.push_back( output ); + + } + else + { + std::shared_ptr output = QgsMeshCalcUtils::create( QgsMeshDatasetGroupMetadata::DataOnFaces ); + output->time = mTimes[0]; + + int facesCount = mMeshLayer->dataProvider()->faceCount(); + output->values.resize( facesCount ); + + for ( int n = 0; n < mMeshLayer->dataProvider()->faceCount(); ++n ) + { + QVector < double > vals; + for ( int datasetIndex = 0; datasetIndex < group1.datasetCount(); ++datasetIndex ) + { + const std::shared_ptr o1 = canditateDataset( group1, datasetIndex ); + double val1 = o1->values[n].scalar(); + if ( !isNoData( val1 ) ) + { + vals.push_back( val1 ); + } + } + + double res_val = D_NODATA; + if ( !vals.isEmpty() ) + { + res_val = func( vals ); + } + + output->values[n] = res_val; + } + + group1.datasets.clear(); + group1.datasets.push_back( output ); + } +} + +const QgsTriangularMesh *QgsMeshCalcUtils::triangularMesh() const +{ + updateMesh(); + Q_ASSERT( mMeshLayer->triangularMesh() ); + return mMeshLayer->triangularMesh(); +} + +const QgsMesh *QgsMeshCalcUtils::nativeMesh() const +{ + updateMesh(); + Q_ASSERT( mMeshLayer->nativeMesh() ); + return mMeshLayer->nativeMesh(); +} + +void QgsMeshCalcUtils::updateMesh() const +{ + if ( ! mMeshLayer->nativeMesh() ) + { + // we do not care about triangles, + // we just want transformed coordinates + // of the native mesh. So create + // some dummy triangular mesh. + QgsMapSettings mapSettings; + mapSettings.setExtent( mMeshLayer->extent() ); + mapSettings.setDestinationCrs( mMeshLayer->crs() ); + mapSettings.setOutputDpi( 96 ); + QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings ); + mMeshLayer->createMapRenderer( context ); + } +} + +void QgsMeshCalcUtils::addIf( QgsMeshMemoryDatasetGroup &trueGroup, + const QgsMeshMemoryDatasetGroup &falseGroup, + const QgsMeshMemoryDatasetGroup &condition ) const +{ + Q_ASSERT( isValid() ); + + // Make sure we have enough outputs in the resulting dataset + expand( trueGroup, condition ); + expand( trueGroup, falseGroup ); + + Q_ASSERT( trueGroup.type == falseGroup.type ); // we do not support mixed output types + Q_ASSERT( trueGroup.type == condition.type ); // we do not support mixed output types + + for ( int time_index = 0; time_index < trueGroup.datasetCount(); ++time_index ) + { + std::shared_ptr true_o = canditateDataset( trueGroup, time_index ); + std::shared_ptr false_o = constCandidateDataset( falseGroup, time_index ); + std::shared_ptr condition_o = constCandidateDataset( condition, time_index ); + for ( int n = 0; n < true_o->values.size(); ++n ) + { + double conditionValue = condition_o->values[n].scalar(); + double resultValue = D_NODATA; + if ( !isNoData( conditionValue ) ) + { + if ( equals( conditionValue, D_TRUE ) ) + resultValue = true_o->values[n].scalar(); + else + resultValue = false_o->values[n].scalar(); + } + true_o->values[n] = resultValue; + } + + if ( trueGroup.type == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + // This is not ideal, as we do not check for true/false branch here in activate + // problem is that activate is on elements, but condition is on nodes... + activate( true_o, condition_o ); + } + } +} + + +void QgsMeshCalcUtils::activate( QgsMeshMemoryDatasetGroup &group ) const +{ + Q_ASSERT( isValid() ); + + if ( mOutputType == QgsMeshDatasetGroupMetadata::DataOnVertices ) + { + for ( int datasetIndex = 0; datasetIndex < group.datasetCount(); ++datasetIndex ) + { + std::shared_ptr o1 = canditateDataset( group, datasetIndex ); + Q_ASSERT( group.type == QgsMeshDatasetGroupMetadata::DataOnVertices ); + activate( o1 ); + } + } + // Groups with data on faces do not have activate flags +} + +void QgsMeshCalcUtils::activate( + std::shared_ptr dataset, + std::shared_ptr refDataset /*=0*/ +) const +{ + + Q_ASSERT( isValid() ); + Q_ASSERT( dataset ); + + // Activate only faces that ha some data and all vertices + for ( int idx = 0; idx < mMeshLayer->dataProvider()->faceCount(); ++idx ) + { + if ( refDataset && !refDataset->active.isEmpty() && ( !refDataset->active[idx] ) ) + { + dataset->active[idx] = false; + continue; + } + + if ( !dataset->active[idx] ) + { + continue; + } + + QgsMeshFace face = nativeMesh()->face( idx ); + + bool isActive = true; //ACTIVE + for ( int j = 0; j < face.size(); ++j ) + { + if ( isNoData( dataset->values[face[j]].scalar() ) ) + { + isActive = false; //NOT ACTIVE + break; + } + } + dataset->active[idx] = isActive; + } +} + +double QgsMeshCalcUtils::ffilter( double val1, double filter ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + + if ( equals( filter, D_TRUE ) ) + return val1; + else + return D_NODATA; +} + +double QgsMeshCalcUtils::fadd( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + return val1 + val2; + +} + +double QgsMeshCalcUtils::fsubtract( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + return val1 - val2; + +} + +double QgsMeshCalcUtils::fmultiply( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + return val1 * val2; + +} + +double QgsMeshCalcUtils::fdivide( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( equals( val2, 0.0 ) ) + return D_NODATA; + else + return val1 / val2; + +} + +double QgsMeshCalcUtils::fpower( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + return pow( val1, val2 ); + +} + +double QgsMeshCalcUtils::fequal( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( equals( val1, val2 ) ) + { + return D_TRUE; + } + else + { + return D_FALSE; + } + +} + +double QgsMeshCalcUtils::fnotEqual( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( equals( val1, val2 ) ) + { + return D_FALSE; + } + else + { + return D_TRUE; + } + +} + +double QgsMeshCalcUtils::fgreaterThan( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( val1 > val2 ) + { + return D_TRUE; + } + else + { + return D_FALSE; + } + +} + +double QgsMeshCalcUtils::flesserThan( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( val1 < val2 ) + { + return D_TRUE; + } + else + { + return D_FALSE; + } + +} + +double QgsMeshCalcUtils::flesserEqual( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( val1 <= val2 ) + { + return D_TRUE; + } + else + { + return D_FALSE; + } + +} + +double QgsMeshCalcUtils::fgreaterEqual( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( val1 >= val2 ) + { + return D_TRUE; + } + else + { + return D_FALSE; + } + +} + + +double QgsMeshCalcUtils::flogicalAnd( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + bool bval1 = equals( val1, D_TRUE ); + bool bval2 = equals( val2, D_TRUE ); + if ( bval1 && bval2 ) + return D_TRUE; + else + return D_FALSE; + +} + +double QgsMeshCalcUtils::flogicalOr( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + bool bval1 = equals( val1, D_TRUE ); + bool bval2 = equals( val2, D_TRUE ); + if ( bval1 || bval2 ) + return D_TRUE; + else + return D_FALSE; + +} + +double QgsMeshCalcUtils::flogicalNot( double val1 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + bool bval1 = equals( val1, D_TRUE ); + if ( bval1 ) + return D_FALSE; + else + return D_TRUE; + +} + +double QgsMeshCalcUtils::fchangeSign( double val1 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + return -val1; +} + +double QgsMeshCalcUtils::fmin( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + if ( val1 > val2 ) + { + return val2; + } + else + { + return val1; + } +} + + +double QgsMeshCalcUtils::fmax( double val1, double val2 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + Q_ASSERT( !isNoData( val2 ) ); + if ( val1 < val2 ) + { + return val2; + } + else + { + return val1; + } + +} + +double QgsMeshCalcUtils::fabs( double val1 ) const +{ + Q_ASSERT( !isNoData( val1 ) ); + if ( val1 > 0 ) + { + return val1; + } + else + { + return -val1; + } + +} + +double QgsMeshCalcUtils::fsumAggregated( QVector &vals ) const +{ + Q_ASSERT( !vals.contains( D_NODATA ) ); + Q_ASSERT( !vals.isEmpty() ); + return std::accumulate( vals.begin(), vals.end(), 0.0 ); +} + +double QgsMeshCalcUtils::fminimumAggregated( QVector &vals ) const +{ + Q_ASSERT( !vals.contains( D_NODATA ) ); + Q_ASSERT( !vals.isEmpty() ); + return *std::min_element( vals.begin(), vals.end() ); +} + +double QgsMeshCalcUtils::fmaximumAggregated( QVector &vals ) const +{ + Q_ASSERT( !vals.contains( D_NODATA ) ); + Q_ASSERT( !vals.isEmpty() ); + return *std::max_element( vals.begin(), vals.end() ); +} + +double QgsMeshCalcUtils::faverageAggregated( QVector &vals ) const +{ + Q_ASSERT( !vals.contains( D_NODATA ) ); + Q_ASSERT( !vals.isEmpty() ); + return fsumAggregated( vals ) / vals.size(); +} + +void QgsMeshCalcUtils::logicalNot( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return func1( group1, std::bind( & QgsMeshCalcUtils::flogicalNot, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::changeSign( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return func1( group1, std::bind( & QgsMeshCalcUtils::fchangeSign, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::abs( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return func1( group1, std::bind( & QgsMeshCalcUtils::fabs, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::add( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fadd, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::subtract( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fsubtract, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::multiply( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fmultiply, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::divide( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fdivide, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::power( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fpower, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::equal( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fequal, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::notEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fnotEqual, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::greaterThan( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fgreaterThan, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::lesserThan( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::flesserThan, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::lesserEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::flesserEqual, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::greaterEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fgreaterEqual, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::logicalAnd( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::flogicalAnd, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::logicalOr( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::flogicalOr, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::minimum( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fmin, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::maximum( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const +{ + return func2( group1, group2, std::bind( & QgsMeshCalcUtils::fmax, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::filter( QgsMeshMemoryDatasetGroup &group1, const QgsRectangle &extent ) const +{ + QgsMeshMemoryDatasetGroup filter( "filter" ); + populateSpatialFilter( filter, extent ); + return func2( group1, filter, std::bind( & QgsMeshCalcUtils::ffilter, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::filter( QgsMeshMemoryDatasetGroup &group1, const QgsGeometry &mask ) const +{ + QgsMeshMemoryDatasetGroup filter( "filter" ); + populateMaskFilter( filter, mask ); + return func2( group1, filter, std::bind( & QgsMeshCalcUtils::ffilter, this, std::placeholders::_1, std::placeholders::_2 ) ); +} + +void QgsMeshCalcUtils::sumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return funcAggr( group1, std::bind( & QgsMeshCalcUtils::fsumAggregated, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::minimumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return funcAggr( group1, std::bind( & QgsMeshCalcUtils::fminimumAggregated, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::maximumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return funcAggr( group1, std::bind( & QgsMeshCalcUtils::fmaximumAggregated, this, std::placeholders::_1 ) ); +} + +void QgsMeshCalcUtils::averageAggregated( QgsMeshMemoryDatasetGroup &group1 ) const +{ + return funcAggr( group1, std::bind( & QgsMeshCalcUtils::faverageAggregated, this, std::placeholders::_1 ) ); +} + +///@endcond diff --git a/src/analysis/mesh/qgsmeshcalcutils.h b/src/analysis/mesh/qgsmeshcalcutils.h new file mode 100644 index 000000000000..4f8858b514d7 --- /dev/null +++ b/src/analysis/mesh/qgsmeshcalcutils.h @@ -0,0 +1,281 @@ +/*************************************************************************** + qgsmeshcalcutils.h + ------------------ + begin : December 18th, 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSMESHCALCUTILS_H +#define QGSMESHCALCUTILS_H + +#define SIP_NO_FILE + +///@cond PRIVATE + +#include +#include +#include + +#include +#include +#include +#include + +#include "qgsrectangle.h" +#include "qgsmeshlayer.h" +#include "qgsmeshdataprovider.h" +#include "qgis_analysis.h" + +struct QgsMeshMemoryDatasetGroup; +struct QgsMeshMemoryDataset; + +/** + * \ingroup analysis + * \class QgsMeshCalcUtils + * Mathematical operations on QgsMeshMemoryDatasetGroup + * QgsMeshMemoryDatasetGroups must be compatible (same mesh structure, same number of datasets, ...) + * Any operation with NODATA is NODATA (e.g. NODATA + 1 = NODATA) + * + * \since QGIS 3.6 + */ +class ANALYSIS_EXPORT QgsMeshCalcUtils +{ + public: + + /** + * Creates the utils and validates the input + * \param layer mesh layer + * \param usedGroupNames dataset group's names that are used in the expression + * \param startTime start time + * \param endTime end time + */ + QgsMeshCalcUtils( QgsMeshLayer *layer, + const QStringList &usedGroupNames, + double startTime, + double endTime ); + + //! Returns whether the input parameters are consistent and valid for given mesh layer + bool isValid() const; + + //! Returns associated mesh layer + const QgsMeshLayer *layer() const; + + //! Returns dataset group based on name + std::shared_ptr group( const QString &groupName ) const; + + //! Creates a single dataset with all values set to 1 + void ones( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Creates a single dataset with all values set to NODATA + void nodata( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Returns a single dataset with all values set to val + std::shared_ptr number( double val, double time ) const; + + //! Creates a deepcopy of group with groupName to group1. Does not copy datasets for filtered out times. + void copy( QgsMeshMemoryDatasetGroup &group1, const QString &groupName ) const; + + //! Creates a deepcopy of dataset0 + std::shared_ptr copy( std::shared_ptr dataset0 ) const; + + //! Changes ownership of all datasets from group2 to group1 + void tranferDatasets( QgsMeshMemoryDatasetGroup &group1, QgsMeshMemoryDatasetGroup &group2 ) const; + + //! If group2 has more datasets than group1, duplicates datasets in group1 so it has the same number of datasets as group2 + void expand( QgsMeshMemoryDatasetGroup &group1, + const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Creates a single dataset with custom number + void number( QgsMeshMemoryDatasetGroup &group1, double val ) const; + + //! If condition is true (for given time&point), takes val from trueGroup else from falseGroup + void addIf( QgsMeshMemoryDatasetGroup &trueGroup, + const QgsMeshMemoryDatasetGroup &falseGroup, + const QgsMeshMemoryDatasetGroup &condition ) const; + + //! Creates a spatial filter from extent + void filter( QgsMeshMemoryDatasetGroup &group1, const QgsRectangle &extent ) const; + + //! Creates a spatial filter from geometry + void filter( QgsMeshMemoryDatasetGroup &group1, const QgsGeometry &mask ) const; + + //! Operator NOT + void logicalNot( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator SIGN + void changeSign( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator ABS + void abs( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator aggregated sum (over all datasets) + void sumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator aggregated min (over all datasets) + void minimumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator aggregated max (over all datasets) + void maximumAggregated( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator aggregated average (over all datasets) + void averageAggregated( QgsMeshMemoryDatasetGroup &group1 ) const; + + //! Operator + + void add( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator - + void subtract( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator * + void multiply( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator / + void divide( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator ^ + void power( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator == + void equal( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator != + void notEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator > + void greaterThan( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator < + void lesserThan( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator <= + void lesserEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator >= + void greaterEqual( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator AND + void logicalAnd( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator OR + void logicalOr( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator minimum + void minimum( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + //! Operator maximum + void maximum( QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + private: + double ffilter( double val1, double filter ) const; + double fadd( double val1, double val2 ) const; + double fsubtract( double val1, double val2 ) const; + double fmultiply( double val1, double val2 ) const; + double fdivide( double val1, double val2 ) const; + double fpower( double val1, double val2 ) const; + double fequal( double val1, double val2 ) const; + double fnotEqual( double val1, double val2 ) const; + double fgreaterThan( double val1, double val2 ) const; + double flesserThan( double val1, double val2 ) const; + double flesserEqual( double val1, double val2 ) const; + double fgreaterEqual( double val1, double val2 ) const; + double flogicalAnd( double val1, double val2 ) const; + double flogicalOr( double val1, double val2 ) const; + double flogicalNot( double val1 ) const; + double fchangeSign( double val1 ) const; + double fmin( double val1, double val2 ) const; + double fmax( double val1, double val2 ) const; + double fabs( double val1 ) const; + double fsumAggregated( QVector &vals ) const; + double fminimumAggregated( QVector &vals ) const; + double fmaximumAggregated( QVector &vals ) const; + double faverageAggregated( QVector &vals ) const; + + /** + * Find dataset group in provider with the name and copy all values to + * memory dataset group. Returns nullptr if no such dataset group + * exists + */ + std::shared_ptr create( const QString &datasetGroupName ) const; + + /** + * Creates dataset based on group. Initializes values and active based on group type. + */ + std::shared_ptr create( const QgsMeshMemoryDatasetGroup &grp ) const; + + /** + * Creates dataset with given type. Initializes values and active based on type. + */ + std::shared_ptr create( const QgsMeshDatasetGroupMetadata::DataType type ) const; + + /** + * Returns dataset based on (time) index. If group has only 1 dataset, returns first one + */ + std::shared_ptr canditateDataset( QgsMeshMemoryDatasetGroup &group, + int datasetIndex ) const; + + /** + * Returns dataset based on on (time) index. If group has only 1 dataset, returns first one + */ + std::shared_ptr constCandidateDataset( const QgsMeshMemoryDatasetGroup &group, + int datasetIndex ) const; + + /** + * Returns maximum number of datasets in the groups + */ + int datasetCount( const QgsMeshMemoryDatasetGroup &group1, const QgsMeshMemoryDatasetGroup &group2 ) const; + + /** + * Set active property for vertices in dataset based on: + * if given vertex is active in dataset and refDataset + * if all values in vertices that are referenced by the dataset are not NODATA + */ + void activate( std::shared_ptr dataset, + std::shared_ptr refDataset = nullptr ) const; + + //! Activates all datasets in group + void activate( QgsMeshMemoryDatasetGroup &group ) const; + + //! Creates spatial filter group from rectagle + void populateSpatialFilter( QgsMeshMemoryDatasetGroup &filter, const QgsRectangle &extent ) const; // create a filter from extent + + //! Creates mask filter group from geometry + void populateMaskFilter( QgsMeshMemoryDatasetGroup &filter, const QgsGeometry &mask ) const; // create a filter from mask + + //! Calculates unary operators + void func1( QgsMeshMemoryDatasetGroup &group, + std::function func ) const; + + //! Calculates binary operators + void func2( QgsMeshMemoryDatasetGroup &group1, + const QgsMeshMemoryDatasetGroup &group2, + std::function func ) const; + + //! Calculates unary aggregate operator (e.g. sum of values of one vertex for all times) + void funcAggr( QgsMeshMemoryDatasetGroup &group1, + std::function& )> func ) const; + + const QgsTriangularMesh *triangularMesh() const; + const QgsMesh *nativeMesh() const; + void updateMesh() const; + + QgsMeshLayer *mMeshLayer; //!< Reference mesh + bool mIsValid; //!< All used datasets (in datasetMap) do have outputs for same times & all used dataset names are present in mesh + QgsMeshDatasetGroupMetadata::DataType mOutputType; //!< Mesh can work only with one output types, so you cannot mix + //!< E.g. one dataset with element outputs and one with node outputs + QVector mTimes; + QMap < QString, std::shared_ptr > mDatasetGroupMap; //!< Groups that are referenced in the expression +}; + +///@endcond + +#endif // QGSMESHCALCUTILS_H diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 4b7dfa3a0bc5..22efd02f7c56 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -238,6 +238,7 @@ SET(QGIS_APP_SRCS mesh/qgsmeshrenderervectorsettingswidget.cpp mesh/qgsmeshrendereractivedatasetwidget.cpp mesh/qgsmeshdatasetgrouptreeview.cpp + mesh/qgsmeshcalculatordialog.cpp ) SET (QGIS_APP_MOC_HDRS @@ -463,7 +464,8 @@ SET (QGIS_APP_MOC_HDRS mesh/qgsmeshrenderervectorsettingswidget.h mesh/qgsmeshrendereractivedatasetwidget.h mesh/qgsmeshdatasetgrouptreeview.h - ) + mesh/qgsmeshcalculatordialog.h +) IF (WITH_3D) @@ -655,6 +657,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/app/locator ${CMAKE_SOURCE_DIR}/src/analysis ${CMAKE_SOURCE_DIR}/src/analysis/raster + ${CMAKE_SOURCE_DIR}/src/analysis/mesh ${CMAKE_SOURCE_DIR}/src/core ${CMAKE_SOURCE_DIR}/src/core/annotations ${CMAKE_SOURCE_DIR}/src/core/auth diff --git a/src/app/mesh/qgsmeshcalculatordialog.cpp b/src/app/mesh/qgsmeshcalculatordialog.cpp new file mode 100644 index 000000000000..755ae5edef59 --- /dev/null +++ b/src/app/mesh/qgsmeshcalculatordialog.cpp @@ -0,0 +1,557 @@ +/*************************************************************************** + qgsmeshcalculatordialog.cpp + --------------------------- + begin : January 2019 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsgdalutils.h" +#include "qgsmeshcalculatordialog.h" +#include "qgsproject.h" +#include "qgsmeshcalcnode.h" +#include "qgsmeshdataprovider.h" +#include "qgsmeshlayer.h" +#include "qgssettings.h" +#include "qgsgui.h" +#include "qgsvectorlayer.h" +#include "qgsmaplayerproxymodel.h" +#include "qgswkbtypes.h" +#include "qgsfeatureiterator.h" +#include "qgsmeshrendereractivedatasetwidget.h" + +#include "cpl_string.h" +#include "gdal.h" +#include "qgis.h" + +#include +#include +#include +#include + +QgsMeshCalculatorDialog::QgsMeshCalculatorDialog( QgsMeshLayer *meshLayer, QWidget *parent, Qt::WindowFlags f ) + : QDialog( parent, f ), + mLayer( meshLayer ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); + + cboLayerMask->setFilters( QgsMapLayerProxyModel::PolygonLayer ); + populateAvailableDatasetGroups(); + + connect( mDatasetsListWidget, &QListWidget::itemDoubleClicked, this, &QgsMeshCalculatorDialog::mDatasetsListWidget_doubleClicked ); + connect( mCurrentLayerExtentButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mCurrentLayerExtentButton_clicked ); + connect( mAllTimesButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mAllTimesButton_clicked ); + connect( mExpressionTextEdit, &QTextEdit::textChanged, this, &QgsMeshCalculatorDialog::mExpressionTextEdit_textChanged ); + + connect( useMaskCb, &QCheckBox::stateChanged, this, &QgsMeshCalculatorDialog::toogleExtendMask ); + connect( useExtentCb, &QCheckBox::stateChanged, this, &QgsMeshCalculatorDialog::toogleExtendMask ); + maskBox->setVisible( false ); + + connect( mPlusPushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mPlusPushButton_clicked ); + connect( mMinusPushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMinusPushButton_clicked ); + connect( mLessButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mLessButton_clicked ); + connect( mLesserEqualButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mLesserEqualButton_clicked ); + connect( mMultiplyPushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMultiplyPushButton_clicked ); + connect( mDividePushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mDividePushButton_clicked ); + connect( mGreaterButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mGreaterButton_clicked ); + connect( mGreaterEqualButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mGreaterEqualButton_clicked ); + connect( mOpenBracketPushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mOpenBracketPushButton_clicked ); + connect( mCloseBracketPushButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mCloseBracketPushButton_clicked ); + connect( mEqualButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mEqualButton_clicked ); + connect( mNotEqualButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mNotEqualButton_clicked ); + connect( mMinButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMinButton_clicked ); + connect( mMaxButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMaxButton_clicked ); + connect( mAbsButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mAbsButton_clicked ); + connect( mPowButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mPowButton_clicked ); + connect( mIfButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mIfButton_clicked ); + connect( mAndButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mAndButton_clicked ); + connect( mOrButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mOrButton_clicked ); + connect( mNotButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mNotButton_clicked ); + connect( mSumAggrButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mSumAggrButton_clicked ); + connect( mMaxAggrButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMaxAggrButton_clicked ); + connect( mMinAggrButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mMinAggrButton_clicked ); + connect( mAverageAggrButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mAverageAggrButton_clicked ); + connect( mNoDataButton, &QPushButton::clicked, this, &QgsMeshCalculatorDialog::mNoDataButton_clicked ); + + mExpressionTextEdit->setCurrentFont( QFontDatabase::systemFont( QFontDatabase::FixedFont ) ); + + useFullLayerExtent(); + repopulateTimeCombos(); + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); + + QgsSettings settings; + mOutputDatasetFileWidget->setStorageMode( QgsFileWidget::SaveFile ); + mOutputDatasetFileWidget->setDialogTitle( tr( "Enter mesh dataset file" ) ); + mOutputDatasetFileWidget->setDefaultRoot( settings.value( QStringLiteral( "/MeshCalculator/lastOutputDir" ), QDir::homePath() ).toString() ); + connect( mOutputDatasetFileWidget, &QgsFileWidget::fileChanged, this, [ = ]() { setAcceptButtonState(); } ); +} + +QgsMeshCalculatorDialog::~QgsMeshCalculatorDialog() = default; + + +QString QgsMeshCalculatorDialog::formulaString() const +{ + return mExpressionTextEdit->toPlainText(); +} + +QgsMeshLayer *QgsMeshCalculatorDialog::meshLayer() const +{ + return mLayer; +} + +QString QgsMeshCalculatorDialog::outputFile() const +{ + QString ret = mOutputDatasetFileWidget->filePath(); + return addSuffix( ret ); +} + +QgsRectangle QgsMeshCalculatorDialog::outputExtent() const +{ + const QgsRectangle ret( + mXMinSpinBox->value(), + mYMinSpinBox->value(), + mXMaxSpinBox->value(), + mYMaxSpinBox->value() + ); + return ret; +} + +QgsGeometry QgsMeshCalculatorDialog::maskGeometry() const +{ + QgsVectorLayer *mask_layer = qobject_cast ( cboLayerMask->currentLayer() ); + if ( mask_layer ) + { + return maskGeometry( mask_layer ); + } + return QgsGeometry(); +} + +QgsGeometry QgsMeshCalculatorDialog::maskGeometry( QgsVectorLayer *layer ) const +{ + QgsFeatureIterator it = layer->getFeatures(); + QVector geometries; + QgsFeature feat; + while ( it.nextFeature( feat ) ) + { + geometries.push_back( feat.geometry() ); + } + QgsGeometry ret = QgsGeometry::unaryUnion( geometries ) ; + return ret; +} + +double QgsMeshCalculatorDialog::startTime() const +{ + if ( mStartTimeComboBox->currentIndex() > -1 ) + return mStartTimeComboBox->itemData( mStartTimeComboBox->currentIndex() ).toDouble(); + else + return 0; +} + +double QgsMeshCalculatorDialog::endTime() const +{ + if ( mEndTimeComboBox->currentIndex() > -1 ) + return mEndTimeComboBox->itemData( mEndTimeComboBox->currentIndex() ).toDouble(); + else + return 0; +} + +std::unique_ptr QgsMeshCalculatorDialog::calculator() const +{ + std::unique_ptr calc; + if ( useExtentCb->isChecked() ) + { + calc.reset( + new QgsMeshCalculator( + formulaString(), + outputFile(), + outputExtent(), + startTime(), + endTime(), + meshLayer() + ) + ); + } + else + { + calc.reset( + new QgsMeshCalculator( + formulaString(), + outputFile(), + maskGeometry(), + startTime(), + endTime(), + meshLayer() + ) + ); + } + return calc; +} + +void QgsMeshCalculatorDialog::toogleExtendMask( int state ) +{ + Q_UNUSED( state ); + if ( useMaskCb->checkState() == Qt::Checked ) + { + extendBox->setVisible( false ); + maskBox->setVisible( true ); + } + else + { + extendBox->setVisible( true ); + maskBox->setVisible( false ); + } +} + +void QgsMeshCalculatorDialog::mDatasetsListWidget_doubleClicked( QListWidgetItem *item ) +{ + if ( !item ) + return; + + const QString group = quoteDatasetGroupEntry( item->text() ); + mExpressionTextEdit->insertPlainText( QStringLiteral( " %1 " ).arg( group ) ); +} + +void QgsMeshCalculatorDialog::mCurrentLayerExtentButton_clicked() +{ + useFullLayerExtent(); +} + +void QgsMeshCalculatorDialog::mAllTimesButton_clicked() +{ + useAllTimesFromLayer(); +} + +void QgsMeshCalculatorDialog::mExpressionTextEdit_textChanged() +{ + if ( expressionValid() ) + { + mExpressionValidLabel->setText( tr( "Expression Valid" ) ); + if ( filePathValid() ) + { + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( true ); + return; + } + } + else + { + mExpressionValidLabel->setText( tr( "Expression invalid" ) ); + } + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); +} + +void QgsMeshCalculatorDialog::mPlusPushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " + " ) ); +} + +void QgsMeshCalculatorDialog::mMinusPushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " - " ) ); +} + +void QgsMeshCalculatorDialog::mLessButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " < " ) ); +} + +void QgsMeshCalculatorDialog::mLesserEqualButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " <= " ) ); +} + +void QgsMeshCalculatorDialog::mMultiplyPushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " * " ) ); +} + +void QgsMeshCalculatorDialog::mDividePushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " / " ) ); +} + +void QgsMeshCalculatorDialog::mGreaterButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " > " ) ); +} + +void QgsMeshCalculatorDialog::mGreaterEqualButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " >= " ) ); +} + +void QgsMeshCalculatorDialog::mOpenBracketPushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " ( " ) ); +} + +void QgsMeshCalculatorDialog::mCloseBracketPushButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " ) " ) ); +} + +void QgsMeshCalculatorDialog::mEqualButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " = " ) ); +} + +void QgsMeshCalculatorDialog::mNotEqualButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " != " ) ); +} + +void QgsMeshCalculatorDialog::mMinButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " min ( A , B ) " ) ); +} + +void QgsMeshCalculatorDialog::mMaxButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " max ( A , B ) " ) ); +} + +void QgsMeshCalculatorDialog::mAbsButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " abs ( " ) ); +} + +void QgsMeshCalculatorDialog::mPowButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " ^ " ) ); +} + +void QgsMeshCalculatorDialog::mIfButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " if ( 1 = 1 , NODATA , NODATA ) " ) ); +} + +void QgsMeshCalculatorDialog::mAndButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " and " ) ); +} + +void QgsMeshCalculatorDialog::mOrButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " or " ) ); +} + +void QgsMeshCalculatorDialog::mNotButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " not " ) ); +} + +void QgsMeshCalculatorDialog::mSumAggrButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " sum_aggr ( " ) ); +} + +void QgsMeshCalculatorDialog::mMaxAggrButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " max_aggr ( " ) ); +} + +void QgsMeshCalculatorDialog::mMinAggrButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " min_aggr ( " ) ); +} + +void QgsMeshCalculatorDialog::mAverageAggrButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " average_aggr ( " ) ); +} + +void QgsMeshCalculatorDialog::mNoDataButton_clicked() +{ + mExpressionTextEdit->insertPlainText( QStringLiteral( " NODATA " ) ); +} + +void QgsMeshCalculatorDialog::setAcceptButtonState() +{ + if ( expressionValid() && filePathValid() ) + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( true ); + else + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); +} + +QString QgsMeshCalculatorDialog::quoteDatasetGroupEntry( const QString group ) +{ + QString ret( group ); + ret = QStringLiteral( "\"%1\"" ).arg( ret.replace( "\"", "\\\"" ) ); + return ret; +} + +void QgsMeshCalculatorDialog::populateAvailableDatasetGroups() +{ + if ( !meshLayer() || !meshLayer()->dataProvider() ) + return; + + const QgsMeshDataProvider *dp = meshLayer()->dataProvider(); + Q_ASSERT( dp ); + + for ( int i = 0; i < dp->datasetGroupCount(); ++i ) + { + const QgsMeshDatasetGroupMetadata meta = dp->datasetGroupMetadata( i ); + mDatasetsListWidget->addItem( meta.name() ); + } +} + +bool QgsMeshCalculatorDialog::expressionValid() const +{ + QString errorString; + QgsMeshCalculator::Result result = QgsMeshCalculator::expression_valid( + formulaString(), + meshLayer() + ); + return ( result == QgsMeshCalculator::Success ); +} + +bool QgsMeshCalculatorDialog::filePathValid() const +{ + QString outputPath = outputFile(); + if ( outputPath.isEmpty() ) + return false; + + outputPath = QFileInfo( outputPath ).absolutePath(); + return QFileInfo( outputPath ).isWritable(); +} + +QString QgsMeshCalculatorDialog::addSuffix( const QString fileName ) const +{ + if ( fileName.isEmpty() ) + return fileName; + + // TODO construct list from MDAL and drivers + // that can be used to write data + // for now, MDAL only supports dat files + const QString allowedSuffix = QStringLiteral( ".dat" ); + + if ( fileName.endsWith( allowedSuffix ) ) + return fileName; + + return fileName + allowedSuffix; +} + +void QgsMeshCalculatorDialog::useFullLayerExtent() +{ + QgsMeshLayer *layer = meshLayer(); + if ( !layer ) + return; + + const QgsRectangle layerExtent = layer->extent(); + mXMinSpinBox->setValue( layerExtent.xMinimum() ); + mXMaxSpinBox->setValue( layerExtent.xMaximum() ); + mYMinSpinBox->setValue( layerExtent.yMinimum() ); + mYMaxSpinBox->setValue( layerExtent.yMaximum() ); +} + +void QgsMeshCalculatorDialog::useAllTimesFromLayer() +{ + const QString datasetGroupName = currentDatasetGroup(); + setTimesByDatasetGroupName( datasetGroupName ); +} + +QString QgsMeshCalculatorDialog::currentDatasetGroup() const +{ + const QList items = mDatasetsListWidget->selectedItems(); + if ( !items.empty() ) + return items[0]->text(); + else + return mDatasetsListWidget->item( 0 )->text(); +} + +void QgsMeshCalculatorDialog::setTimesByDatasetGroupName( const QString group ) +{ + QgsMeshLayer *layer = meshLayer(); + if ( !layer || !layer->dataProvider() ) + return; + const QgsMeshDataProvider *dp = layer->dataProvider(); + + // find group index from group name + int groupIndex = -1; + for ( int i = 0; i < dp->datasetGroupCount(); ++i ) + { + const QgsMeshDatasetGroupMetadata meta = dp->datasetGroupMetadata( i ); + if ( meta.name() == group ) + { + groupIndex = i; + break; + } + } + + if ( groupIndex < 0 ) + return; //not found + + int datasetCount = dp->datasetCount( groupIndex ); + if ( datasetCount < 1 ) + return; // group without datasets + + + // find maximum and minimum time in this group + double minTime = dp->datasetMetadata( QgsMeshDatasetIndex( groupIndex, 0 ) ).time(); + int idx = mStartTimeComboBox->findData( minTime ); + if ( idx >= 0 ) + mStartTimeComboBox->setCurrentIndex( idx ); + + double maxTime = dp->datasetMetadata( QgsMeshDatasetIndex( groupIndex, datasetCount - 1 ) ).time(); + idx = mEndTimeComboBox->findData( maxTime ); + if ( idx >= 0 ) + mEndTimeComboBox->setCurrentIndex( idx ); +} + +void QgsMeshCalculatorDialog::repopulateTimeCombos() +{ + QgsMeshLayer *layer = meshLayer(); + if ( !layer || !layer->dataProvider() ) + return; + const QgsMeshDataProvider *dp = layer->dataProvider(); + + // extract all times from all datasets + QMap times; + + for ( int groupIndex = 0; groupIndex < dp->datasetGroupCount(); ++groupIndex ) + { + for ( int datasetIndex = 0; datasetIndex < dp->datasetCount( groupIndex ); ++datasetIndex ) + { + const QgsMeshDatasetMetadata meta = dp->datasetMetadata( QgsMeshDatasetIndex( groupIndex, datasetIndex ) ); + const double time = meta.time(); + + const QString timestr = QgsMeshRendererActiveDatasetWidget::formatTime( time ); // the format is "HH:mm:ss" + + times[timestr] = time; + } + } + + // sort by text + auto keys = times.keys(); + keys.sort(); + + mStartTimeComboBox->blockSignals( true ); + mEndTimeComboBox->blockSignals( true ); + mStartTimeComboBox->clear(); + mEndTimeComboBox->clear(); + + // populate combos + for ( const QString &key : keys ) + { + mStartTimeComboBox->addItem( key, times[key] ); + mEndTimeComboBox->addItem( key, times[key] ); + } + + mStartTimeComboBox->blockSignals( false ); + mEndTimeComboBox->blockSignals( false ); + + + if ( !times.empty() ) + { + mStartTimeComboBox->setCurrentIndex( 0 ); + mEndTimeComboBox->setCurrentIndex( times.size() - 1 ); + } +} diff --git a/src/app/mesh/qgsmeshcalculatordialog.h b/src/app/mesh/qgsmeshcalculatordialog.h new file mode 100644 index 000000000000..4ef6feab9a70 --- /dev/null +++ b/src/app/mesh/qgsmeshcalculatordialog.h @@ -0,0 +1,129 @@ +/*************************************************************************** + qgsmeshcalculatordialog.h + ------------------------- + begin : January 2019 + copyright : (C) 2018 by Peter Petrik + email : zilolv at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSMESHCALCULATORDIALOG_H +#define QGSMESHCALCULATORDIALOG_H + +#include "ui_qgsmeshcalculatordialogbase.h" +#include "qgsmeshcalculator.h" +#include "qgshelp.h" +#include "qgis_app.h" + +//! A dialog to enter a mesh calculation expression +class APP_EXPORT QgsMeshCalculatorDialog: public QDialog, private Ui::QgsMeshCalculatorDialogBase +{ + Q_OBJECT + public: + + /** + * Constructor for raster calculator dialog + * \param meshLayer main mesh layer, will be used for default extent and projection + * \param parent widget + * \param f window flags + */ + QgsMeshCalculatorDialog( QgsMeshLayer *meshLayer = nullptr, QWidget *parent = nullptr, Qt::WindowFlags f = nullptr ); + ~QgsMeshCalculatorDialog(); + + //! Returns new mesh calculator created from dialog options + std::unique_ptr calculator() const; + + private slots: + void mDatasetsListWidget_doubleClicked( QListWidgetItem *item ); + void mCurrentLayerExtentButton_clicked(); + void mAllTimesButton_clicked(); + void mExpressionTextEdit_textChanged(); + void toogleExtendMask( int state ); + + //calculator buttons + void mPlusPushButton_clicked(); + void mMinusPushButton_clicked(); + void mLessButton_clicked(); + void mLesserEqualButton_clicked(); + void mMultiplyPushButton_clicked(); + void mDividePushButton_clicked(); + void mGreaterButton_clicked(); + void mGreaterEqualButton_clicked(); + void mOpenBracketPushButton_clicked(); + void mCloseBracketPushButton_clicked(); + void mEqualButton_clicked(); + void mNotEqualButton_clicked(); + void mMinButton_clicked(); + void mMaxButton_clicked(); + void mAbsButton_clicked(); + void mPowButton_clicked(); + void mIfButton_clicked(); + void mAndButton_clicked(); + void mOrButton_clicked(); + void mNotButton_clicked(); + void mSumAggrButton_clicked(); + void mMaxAggrButton_clicked(); + void mMinAggrButton_clicked(); + void mAverageAggrButton_clicked(); + void mNoDataButton_clicked(); + + private: + QString formulaString() const; + QgsMeshLayer *meshLayer() const; + + QString outputFile() const; + QgsRectangle outputExtent() const; + QgsGeometry maskGeometry() const; + + double startTime() const; + double endTime() const; + + //! Combines geometries from selected vector layer to create mask filter geometry + QgsGeometry maskGeometry( QgsVectorLayer *layer ) const; + + //! Sets widget to match full layer extent + void useFullLayerExtent(); + + //! Sets time combos from current layer + void useAllTimesFromLayer(); + + //! Returns name of the selected dataset group + QString currentDatasetGroup() const; + + //! Sets time combos from selected group + void setTimesByDatasetGroupName( const QString group ); + + //! Sets available times from layer + void repopulateTimeCombos(); + + //! Enables OK button if calculator expression is valid and output file path exists + void setAcceptButtonState(); + + //! Qoutes the dataset group name for calculator + QString quoteDatasetGroupEntry( const QString group ); + + //! Gets all datasets groups from layer and populated list + void populateAvailableDatasetGroups(); + + //! Returns true if mesh calculator expression has valid syntax + bool expressionValid() const; + + //! Returns true if file path is valid and writable + bool filePathValid() const; + + //! Add file suffix if not present + QString addSuffix( const QString fileName ) const; + + QgsMeshLayer *mLayer; + friend class TestQgsMeshCalculatorDialog; +}; + +#endif // QGSMESHCALCULATORDIALOG_H diff --git a/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp b/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp index 7637caf2b259..dbbdd50411ff 100644 --- a/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp +++ b/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp @@ -102,7 +102,7 @@ void QgsMeshRendererActiveDatasetWidget::setTimeRange() QgsMeshDatasetIndex index( groupWithMaximumDatasets, i ); QgsMeshDatasetMetadata meta = mMeshLayer->dataProvider()->datasetMetadata( index ); double time = meta.time(); - mTimeComboBox->addItem( timeToString( time ), time ); + mTimeComboBox->addItem( formatTime( time ), time ); } } mTimeComboBox->blockSignals( false ); @@ -249,12 +249,20 @@ void QgsMeshRendererActiveDatasetWidget::updateMetadata() mActiveDatasetMetadata->setText( msg ); } -QString QgsMeshRendererActiveDatasetWidget::timeToString( double val ) +QString QgsMeshRendererActiveDatasetWidget::formatTime( double hours ) { - // time val should be in hours - int seconds = static_cast( qgsRound( val * 3600.0, 0 ) ); + // time should be in hours + int seconds = static_cast( qgsRound( hours * 3600.0, 0 ) ); + int days = static_cast( floor( hours / 24.0 ) ); QTime t = QTime( 0, 0 ).addSecs( seconds ); - return t.toString(); // the format is "HH:mm:ss" + if ( days > 0 ) + { + return QStringLiteral( "%1 d %2" ).arg( days ).arg( t.toString() ); // the format is "d HH:mm:ss + } + else + { + return t.toString(); // the format is "HH:mm:ss" + } } QString QgsMeshRendererActiveDatasetWidget::metadata( QgsMeshDatasetIndex datasetIndex ) diff --git a/src/app/mesh/qgsmeshrendereractivedatasetwidget.h b/src/app/mesh/qgsmeshrendereractivedatasetwidget.h index 3fb0eeb6f074..f787fc82d36c 100644 --- a/src/app/mesh/qgsmeshrendereractivedatasetwidget.h +++ b/src/app/mesh/qgsmeshrendereractivedatasetwidget.h @@ -61,6 +61,13 @@ class APP_EXPORT QgsMeshRendererActiveDatasetWidget : public QWidget, private Ui //! Synchronizes widgets state with associated mesh layer void syncToLayer(); + /** + * Formats time to human readable string + * \param hours time in double in hours + * \returns formatted time string + */ + static QString formatTime( double hours ); + signals: //! Emitted when the current scalar group gets changed @@ -87,7 +94,6 @@ class APP_EXPORT QgsMeshRendererActiveDatasetWidget : public QWidget, private Ui //! Loop through all dataset groups and find the maximum number of datasets void setTimeRange(); void updateMetadata(); - QString timeToString( double val ); QgsMeshLayer *mMeshLayer = nullptr; // not owned int mActiveScalarDatasetGroup = -1; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 66f7b57da0ae..4f14fbe2edff 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -17,6 +17,7 @@ * * ***************************************************************************/ +#include #include #include #include @@ -270,6 +271,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsproxyprogresstask.h" #include "qgsquerybuilder.h" #include "qgsrastercalcdialog.h" +#include "qgsmeshcalculatordialog.h" #include "qgsrasterfilewriter.h" #include "qgsrasterfilewritertask.h" #include "qgsrasteriterator.h" @@ -2174,6 +2176,7 @@ void QgisApp::createActions() connect( mActionNewGeoPackageLayer, &QAction::triggered, this, &QgisApp::newGeoPackageLayer ); connect( mActionNewMemoryLayer, &QAction::triggered, this, &QgisApp::newMemoryLayer ); connect( mActionShowRasterCalculator, &QAction::triggered, this, &QgisApp::showRasterCalculator ); + connect( mActionShowMeshCalculator, &QAction::triggered, this, &QgisApp::showMeshCalculator ); connect( mActionShowAlignRasterTool, &QAction::triggered, this, &QgisApp::showAlignRasterTool ); connect( mActionEmbedLayers, &QAction::triggered, this, &QgisApp::embedLayers ); connect( mActionAddLayerDefinition, &QAction::triggered, this, &QgisApp::addLayerDefinition ); @@ -3349,6 +3352,7 @@ void QgisApp::setTheme( const QString &themeName ) mActionZoomFullExtent->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionZoomFullExtent.svg" ) ) ); mActionZoomToSelected->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionZoomToSelected.svg" ) ) ); mActionShowRasterCalculator->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowRasterCalculator.png" ) ) ); + mActionShowMeshCalculator->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowMeshCalculator.png" ) ) ); mActionPan->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionPan.svg" ) ) ); mActionPanToSelected->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionPanToSelected.svg" ) ) ); mActionZoomLast->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionZoomLast.svg" ) ) ); @@ -5742,6 +5746,73 @@ void QgisApp::showRasterCalculator() } } +void QgisApp::showMeshCalculator() +{ + QgsMeshCalculatorDialog d( qobject_cast( activeLayer() ), this ); + if ( d.exec() == QDialog::Accepted ) + { + //invoke analysis library + std::unique_ptr calculator = d.calculator(); + + QProgressDialog p( tr( "Calculating mesh expression…" ), tr( "Abort" ), 0, 0 ); + p.setWindowModality( Qt::WindowModal ); + p.setMaximum( 100.0 ); + QgsFeedback feedback; + connect( &feedback, &QgsFeedback::progressChanged, &p, &QProgressDialog::setValue ); + connect( &p, &QProgressDialog::canceled, &feedback, &QgsFeedback::cancel ); + p.show(); + QgsMeshCalculator::Result res = calculator->processCalculation( &feedback ); + switch ( res ) + { + case QgsMeshCalculator::Success: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Calculation complete." ), + Qgis::Info, messageTimeout() ); + break; + + case QgsMeshCalculator::EvaluateError: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Could not evaluate the formula." ), + Qgis::Critical, messageTimeout() ); + break; + + case QgsMeshCalculator::InvalidDatasets: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Invalid or incompatible datasets used." ), + Qgis::Critical, messageTimeout() ); + break; + + case QgsMeshCalculator::CreateOutputError: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Could not create destination file." ), + Qgis::Critical ); + break; + + case QgsMeshCalculator::InputLayerError: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Could not read input layer." ), + Qgis::Critical ); + break; + + case QgsMeshCalculator::Canceled: + break; + + case QgsMeshCalculator::ParserError: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Could not parse mesh formula." ), + Qgis::Critical ); + break; + + case QgsMeshCalculator::MemoryError: + messageBar()->pushMessage( tr( "Mesh calculator" ), + tr( "Insufficient memory available for operation." ), + Qgis::Critical ); + break; + } + p.hide(); + } +} + void QgisApp::showAlignRasterTool() { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index bf74ca4a83d2..cd19e8741781 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -1318,6 +1318,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void fileNewFromDefaultTemplate(); //! Calculate new rasters from existing ones void showRasterCalculator(); + //! Calculate new meshes from existing ones + void showMeshCalculator(); //! Open dialog to align raster layers void showAlignRasterTool(); void embedLayers(); diff --git a/src/core/mesh/qgsmeshdataprovider.h b/src/core/mesh/qgsmeshdataprovider.h index c32f001bf684..3527ce67b9c2 100644 --- a/src/core/mesh/qgsmeshdataprovider.h +++ b/src/core/mesh/qgsmeshdataprovider.h @@ -509,6 +509,28 @@ class CORE_EXPORT QgsMeshDatasetSourceInterface SIP_ABSTRACT * \since QGIS 3.6 */ virtual QgsMeshDataBlock areFacesActive( QgsMeshDatasetIndex index, int faceIndex, int count ) const = 0; + + /** + * Creates a new dataset group from a data and + * persists it into a destination path + * + * On success, the mesh's dataset group count is changed + * + * \param path destination path of the stored file + * \param meta new group's metadata + * \param datasetValues scalar/vector values for all datasets and all faces/vertices in the group + * \param datasetActive active flag values for all datasets in the group. Empty array represents can be used + * when all faces are active + * \returns true on failure, false on success + * + * \since QGIS 3.6 + */ + virtual bool persistDatasetGroup( const QString &path, + const QgsMeshDatasetGroupMetadata &meta, + const QVector &datasetValues, + const QVector &datasetActive, + const QVector × + ) = 0; }; diff --git a/src/core/mesh/qgsmeshlayer.cpp b/src/core/mesh/qgsmeshlayer.cpp index 24c225420030..c6832162210f 100644 --- a/src/core/mesh/qgsmeshlayer.cpp +++ b/src/core/mesh/qgsmeshlayer.cpp @@ -103,12 +103,21 @@ QgsMesh *QgsMeshLayer::nativeMesh() SIP_SKIP return mNativeMesh.get(); } +const QgsMesh *QgsMeshLayer::nativeMesh() const SIP_SKIP +{ + return mNativeMesh.get(); +} QgsTriangularMesh *QgsMeshLayer::triangularMesh() SIP_SKIP { return mTriangularMesh.get(); } +const QgsTriangularMesh *QgsMeshLayer::triangularMesh() const SIP_SKIP +{ + return mTriangularMesh.get(); +} + QgsMeshLayerRendererCache *QgsMeshLayer::rendererCache() { return mRendererCache.get(); diff --git a/src/core/mesh/qgsmeshlayer.h b/src/core/mesh/qgsmeshlayer.h index 763b40d0b598..f9a462390bab 100644 --- a/src/core/mesh/qgsmeshlayer.h +++ b/src/core/mesh/qgsmeshlayer.h @@ -141,9 +141,15 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer //! Returns native mesh (nullptr before rendering) QgsMesh *nativeMesh() SIP_SKIP; + //! Returns native mesh (nullptr before rendering) + const QgsMesh *nativeMesh() const SIP_SKIP; + //! Returns triangular mesh (nullptr before rendering) QgsTriangularMesh *triangularMesh() SIP_SKIP; + //! Returns triangular mesh (nullptr before rendering) + const QgsTriangularMesh *triangularMesh() const SIP_SKIP; + //! Returns native mesh (nullptr before rendering) QgsMeshLayerRendererCache *rendererCache() SIP_SKIP; diff --git a/src/core/mesh/qgsmeshmemorydataprovider.cpp b/src/core/mesh/qgsmeshmemorydataprovider.cpp index 5ebc9b9abde3..1b1ee7f15336 100644 --- a/src/core/mesh/qgsmeshmemorydataprovider.cpp +++ b/src/core/mesh/qgsmeshmemorydataprovider.cpp @@ -166,10 +166,10 @@ bool QgsMeshMemoryDataProvider::splitDatasetSections( const QString &uri, QgsMes { if ( !success ) break; - QgsMeshMemoryDataset dataset; + std::shared_ptr dataset = std::make_shared(); success = addDatasetValues( sections[i], dataset, datasetGroup.isScalar ); if ( success ) - success = checkDatasetValidity( dataset, datasetGroup.isOnVertices ); + success = checkDatasetValidity( dataset, datasetGroup.type == QgsMeshDatasetGroupMetadata::DataOnVertices ); if ( success ) datasetGroup.datasets.push_back( dataset ); } @@ -188,7 +188,11 @@ bool QgsMeshMemoryDataProvider::setDatasetGroupType( const QString &def, QgsMesh return false; } - datasetGroup.isOnVertices = 0 == QString::compare( types[0].trimmed(), QStringLiteral( "vertex" ), Qt::CaseInsensitive ); + if ( 0 == QString::compare( types[0].trimmed(), QStringLiteral( "vertex" ), Qt::CaseInsensitive ) ) + datasetGroup.type = QgsMeshDatasetGroupMetadata::DataOnVertices; + else + datasetGroup.type = QgsMeshDatasetGroupMetadata::DataOnFaces; + datasetGroup.isScalar = 0 == QString::compare( types[1].trimmed(), QStringLiteral( "scalar" ), Qt::CaseInsensitive ); datasetGroup.name = types[2].trimmed(); @@ -213,7 +217,7 @@ bool QgsMeshMemoryDataProvider::addDatasetGroupMetadata( const QString &def, Qgs return true; } -bool QgsMeshMemoryDataProvider::addDatasetValues( const QString &def, QgsMeshMemoryDataset &dataset, bool isScalar ) +bool QgsMeshMemoryDataProvider::addDatasetValues( const QString &def, std::shared_ptr &dataset, bool isScalar ) { const QStringList valuesLines = def.split( '\n', QString::SkipEmptyParts ); // first line is time @@ -224,7 +228,7 @@ bool QgsMeshMemoryDataProvider::addDatasetValues( const QString &def, QgsMeshMem return false; } - dataset.time = valuesLines[0].toDouble(); + dataset->time = valuesLines[0].toDouble(); for ( int i = 1; i < valuesLines.size(); ++i ) { @@ -259,36 +263,36 @@ bool QgsMeshMemoryDataProvider::addDatasetValues( const QString &def, QgsMeshMem } } - dataset.values.push_back( point ); + dataset->values.push_back( point ); } return true; } -bool QgsMeshMemoryDataProvider::checkDatasetValidity( QgsMeshMemoryDataset &dataset, bool isOnVertices ) +bool QgsMeshMemoryDataProvider::checkDatasetValidity( std::shared_ptr &dataset, bool isOnVertices ) { bool valid = true; if ( isOnVertices ) { - if ( dataset.values.count() != vertexCount() ) + if ( dataset->values.count() != vertexCount() ) { valid = false; - setError( QgsError( tr( "Dataset defined on vertices has {} values, but mesh {}" ).arg( dataset.values.count(), vertexCount() ), + setError( QgsError( tr( "Dataset defined on vertices has {} values, but mesh {}" ).arg( dataset->values.count(), vertexCount() ), QStringLiteral( "Mesh Memory Provider" ) ) ); } } else { // on faces - if ( dataset.values.count() != faceCount() ) + if ( dataset->values.count() != faceCount() ) { valid = false; - setError( QgsError( tr( "Dataset defined on faces has {} values, but mesh {}" ).arg( dataset.values.count(), faceCount() ), + setError( QgsError( tr( "Dataset defined on faces has {} values, but mesh {}" ).arg( dataset->values.count(), faceCount() ), QStringLiteral( "Mesh Memory Provider" ) ) ); } } - dataset.valid = valid; + dataset->valid = valid; return valid; } @@ -366,15 +370,7 @@ QgsMeshDatasetGroupMetadata QgsMeshMemoryDataProvider::datasetGroupMetadata( int { if ( ( groupIndex >= 0 ) && ( groupIndex < datasetGroupCount() ) ) { - QgsMeshDatasetGroupMetadata metadata( - mDatasetGroups[groupIndex].name, - mDatasetGroups[groupIndex].isScalar, - mDatasetGroups[groupIndex].isOnVertices, - mDatasetGroups[groupIndex].minimum, - mDatasetGroups[groupIndex].maximum, - mDatasetGroups[groupIndex].metadata - ); - return metadata; + return mDatasetGroups[groupIndex].groupMetadata(); } else { @@ -382,6 +378,38 @@ QgsMeshDatasetGroupMetadata QgsMeshMemoryDataProvider::datasetGroupMetadata( int } } +QgsMeshDatasetGroupMetadata QgsMeshMemoryDatasetGroup::groupMetadata() const +{ + return QgsMeshDatasetGroupMetadata( + name, + isScalar, + type == QgsMeshDatasetGroupMetadata::DataOnVertices, + minimum, + maximum, + metadata + ); +} + +int QgsMeshMemoryDatasetGroup::datasetCount() const +{ + return datasets.size(); +} + +void QgsMeshMemoryDatasetGroup::addDataset( std::shared_ptr dataset ) +{ + datasets.push_back( dataset ); +} + +void QgsMeshMemoryDatasetGroup::clearDatasets() +{ + datasets.clear(); +} + +std::shared_ptr QgsMeshMemoryDatasetGroup::constDataset( int index ) const +{ + return datasets[index]; +} + QgsMeshDatasetMetadata QgsMeshMemoryDataProvider::datasetMetadata( QgsMeshDatasetIndex index ) const { if ( ( index.group() >= 0 ) && ( index.group() < datasetGroupCount() ) && @@ -390,10 +418,10 @@ QgsMeshDatasetMetadata QgsMeshMemoryDataProvider::datasetMetadata( QgsMeshDatase { const QgsMeshMemoryDatasetGroup &grp = mDatasetGroups.at( index.group() ); QgsMeshDatasetMetadata metadata( - grp.datasets[index.dataset()].time, - grp.datasets[index.dataset()].valid, - grp.datasets[index.dataset()].minimum, - grp.datasets[index.dataset()].maximum + grp.datasets[index.dataset()]->time, + grp.datasets[index.dataset()]->valid, + grp.datasets[index.dataset()]->minimum, + grp.datasets[index.dataset()]->maximum ); return metadata; } @@ -407,9 +435,9 @@ QgsMeshDatasetValue QgsMeshMemoryDataProvider::datasetValue( QgsMeshDatasetIndex { if ( ( index.group() >= 0 ) && ( index.group() < datasetGroupCount() ) && ( index.dataset() >= 0 ) && ( index.dataset() < datasetCount( index.group() ) ) && - ( valueIndex >= 0 ) && ( valueIndex < mDatasetGroups[index.group()].datasets[index.dataset()].values.count() ) ) + ( valueIndex >= 0 ) && ( valueIndex < mDatasetGroups[index.group()].datasets[index.dataset()]->values.count() ) ) { - return mDatasetGroups[index.group()].datasets[index.dataset()].values[valueIndex]; + return mDatasetGroups[index.group()].datasets[index.dataset()]->values[valueIndex]; } else { @@ -419,13 +447,37 @@ QgsMeshDatasetValue QgsMeshMemoryDataProvider::datasetValue( QgsMeshDatasetIndex QgsMeshDataBlock QgsMeshMemoryDataProvider::datasetValues( QgsMeshDatasetIndex index, int valueIndex, int count ) const { - bool isScalar = mDatasetGroups[index.group()].isScalar; + if ( ( index.group() >= 0 ) && ( index.group() < datasetGroupCount() ) ) + { + const QgsMeshMemoryDatasetGroup group = mDatasetGroups[index.group()]; + bool isScalar = group.isScalar; + if ( ( index.dataset() >= 0 ) && ( index.dataset() < group.datasets.size() ) ) + { + return group.datasets[index.dataset()]->datasetValues( isScalar, valueIndex, count ); + } + else + { + return QgsMeshDataBlock(); + } + } + else + { + return QgsMeshDataBlock(); + } +} + +QgsMeshDataBlock QgsMeshMemoryDataset::datasetValues( bool isScalar, int valueIndex, int count ) const +{ QgsMeshDataBlock ret( isScalar ? QgsMeshDataBlock::ScalarDouble : QgsMeshDataBlock::Vector2DDouble, count ); double *buf = static_cast( ret.buffer() ); for ( int i = 0; i < count; ++i ) { - QgsMeshDatasetValue val = datasetValue( index, valueIndex + i ); + int idx = valueIndex + i; + if ( ( idx < 0 ) || ( idx >= values.size() ) ) + return ret; + + QgsMeshDatasetValue val = values[ valueIndex + i ]; if ( isScalar ) buf[i] = val.x(); else @@ -439,20 +491,62 @@ QgsMeshDataBlock QgsMeshMemoryDataProvider::datasetValues( QgsMeshDatasetIndex i bool QgsMeshMemoryDataProvider::isFaceActive( QgsMeshDatasetIndex index, int faceIndex ) const { - Q_UNUSED( index ); - Q_UNUSED( faceIndex ); - return true; + if ( mDatasetGroups[index.group()].datasets[index.dataset()]->active.isEmpty() ) + return true; + else + return mDatasetGroups[index.group()].datasets[index.dataset()]->active[faceIndex]; } QgsMeshDataBlock QgsMeshMemoryDataProvider::areFacesActive( QgsMeshDatasetIndex index, int faceIndex, int count ) const { - Q_UNUSED( index ); - Q_UNUSED( faceIndex ); + if ( ( index.group() >= 0 ) && ( index.group() < datasetGroupCount() ) ) + { + const QgsMeshMemoryDatasetGroup group = mDatasetGroups[index.group()]; + if ( ( index.dataset() >= 0 ) && ( index.dataset() < group.datasets.size() ) ) + { + return group.datasets[index.dataset()]->areFacesActive( faceIndex, count ); + } + else + { + return QgsMeshDataBlock(); + } + } + else + { + return QgsMeshDataBlock(); + } +} + +QgsMeshDataBlock QgsMeshMemoryDataset::areFacesActive( int faceIndex, int count ) const +{ QgsMeshDataBlock ret( QgsMeshDataBlock::ActiveFlagInteger, count ); - memset( ret.buffer(), 1, static_cast( count ) * sizeof( int ) ); + if ( active.isEmpty() || + ( faceIndex < 0 ) || + ( faceIndex + count > active.size() ) + ) + memset( ret.buffer(), 1, static_cast( count ) * sizeof( int ) ); + else + memcpy( ret.buffer(), + active.data() + faceIndex, + static_cast( count ) * sizeof( int ) ); + return ret; } +bool QgsMeshMemoryDataProvider::persistDatasetGroup( const QString &path, + const QgsMeshDatasetGroupMetadata &meta, + const QVector &datasetValues, + const QVector &datasetActive, + const QVector × ) +{ + Q_UNUSED( path ); + Q_UNUSED( meta ); + Q_UNUSED( datasetValues ); + Q_UNUSED( datasetActive ); + Q_UNUSED( times ); + return true; // not implemented/supported +} + void QgsMeshMemoryDataProvider::calculateMinMaxForDatasetGroup( QgsMeshMemoryDatasetGroup &grp ) const { double min = std::numeric_limits::max(); @@ -462,28 +556,28 @@ void QgsMeshMemoryDataProvider::calculateMinMaxForDatasetGroup( QgsMeshMemoryDat for ( int i = 0; i < count; ++i ) { calculateMinMaxForDataset( grp.datasets[i] ); - min = std::min( min, grp.datasets[i].minimum ); - max = std::max( max, grp.datasets[i].maximum ); + min = std::min( min, grp.datasets[i]->minimum ); + max = std::max( max, grp.datasets[i]->maximum ); } grp.minimum = min; grp.maximum = max; } -void QgsMeshMemoryDataProvider::calculateMinMaxForDataset( QgsMeshMemoryDataset &dataset ) const +void QgsMeshMemoryDataProvider::calculateMinMaxForDataset( std::shared_ptr &dataset ) const { double min = std::numeric_limits::max(); double max = std::numeric_limits::min(); - if ( !dataset.valid ) + if ( !dataset->valid ) { return; } bool firstIteration = true; - for ( int i = 0; i < dataset.values.size(); ++i ) + for ( int i = 0; i < dataset->values.size(); ++i ) { - double v = dataset.values[i].scalar(); + double v = dataset->values[i].scalar(); if ( std::isnan( v ) ) continue; @@ -506,8 +600,8 @@ void QgsMeshMemoryDataProvider::calculateMinMaxForDataset( QgsMeshMemoryDataset } } - dataset.minimum = min; - dataset.maximum = max; + dataset->minimum = min; + dataset->maximum = max; } QgsRectangle QgsMeshMemoryDataProvider::calculateExtent() const @@ -523,4 +617,14 @@ QgsRectangle QgsMeshMemoryDataProvider::calculateExtent() const } return rec; } + + +QgsMeshMemoryDatasetGroup::QgsMeshMemoryDatasetGroup( const QString &nm ): + name( nm ) +{ +} + +QgsMeshMemoryDatasetGroup::QgsMeshMemoryDatasetGroup() = default; +QgsMeshMemoryDataset::QgsMeshMemoryDataset() = default; + ///@endcond diff --git a/src/core/mesh/qgsmeshmemorydataprovider.h b/src/core/mesh/qgsmeshmemorydataprovider.h index 83bf6d952ab5..cbe6975a0689 100644 --- a/src/core/mesh/qgsmeshmemorydataprovider.h +++ b/src/core/mesh/qgsmeshmemorydataprovider.h @@ -23,28 +23,42 @@ ///@cond PRIVATE #include +#include #include "qgis_core.h" #include "qgis.h" #include "qgsmeshdataprovider.h" #include "qgsrectangle.h" -struct QgsMeshMemoryDataset +struct CORE_EXPORT QgsMeshMemoryDataset { + QgsMeshMemoryDataset(); + QgsMeshDataBlock datasetValues( bool isScalar, int valueIndex, int count ) const; + QgsMeshDataBlock areFacesActive( int faceIndex, int count ) const; + QVector values; + QVector active; double time = -1; bool valid = false; double minimum = std::numeric_limits::quiet_NaN(); double maximum = std::numeric_limits::quiet_NaN(); }; -struct QgsMeshMemoryDatasetGroup +struct CORE_EXPORT QgsMeshMemoryDatasetGroup { + QgsMeshMemoryDatasetGroup( const QString &nm ); + QgsMeshMemoryDatasetGroup(); + QgsMeshDatasetGroupMetadata groupMetadata() const; + int datasetCount() const; + void addDataset( std::shared_ptr dataset ); + void clearDatasets(); + std::shared_ptr constDataset( int index ) const; + QMap metadata; - QVector datasets; + QVector> datasets; QString name; bool isScalar = true; - bool isOnVertices = true; + QgsMeshDatasetGroupMetadata::DataType type = QgsMeshDatasetGroupMetadata::DataOnVertices; double minimum = std::numeric_limits::quiet_NaN(); double maximum = std::numeric_limits::quiet_NaN(); }; @@ -54,7 +68,7 @@ struct QgsMeshMemoryDatasetGroup * Provides data stored in-memory for QgsMeshLayer. Useful for plugins or tests. * \since QGIS 3.2 */ -class QgsMeshMemoryDataProvider: public QgsMeshDataProvider +class CORE_EXPORT QgsMeshMemoryDataProvider: public QgsMeshDataProvider { Q_OBJECT @@ -134,6 +148,12 @@ class QgsMeshMemoryDataProvider: public QgsMeshDataProvider QgsMeshDataBlock datasetValues( QgsMeshDatasetIndex index, int valueIndex, int count ) const override; bool isFaceActive( QgsMeshDatasetIndex index, int faceIndex ) const override; QgsMeshDataBlock areFacesActive( QgsMeshDatasetIndex index, int faceIndex, int count ) const override; + bool persistDatasetGroup( const QString &path, + const QgsMeshDatasetGroupMetadata &meta, + const QVector &datasetValues, + const QVector &datasetActive, + const QVector × + ) override; //! Returns the memory provider key static QString providerKey(); @@ -144,7 +164,7 @@ class QgsMeshMemoryDataProvider: public QgsMeshDataProvider private: void calculateMinMaxForDatasetGroup( QgsMeshMemoryDatasetGroup &grp ) const; - void calculateMinMaxForDataset( QgsMeshMemoryDataset &dataset ) const; + void calculateMinMaxForDataset( std::shared_ptr &dataset ) const; QgsRectangle calculateExtent( ) const; bool splitMeshSections( const QString &uri ); @@ -154,8 +174,8 @@ class QgsMeshMemoryDataProvider: public QgsMeshDataProvider bool splitDatasetSections( const QString &uri, QgsMeshMemoryDatasetGroup &datasetGroup ); bool setDatasetGroupType( const QString &uri, QgsMeshMemoryDatasetGroup &datasetGroup ); bool addDatasetGroupMetadata( const QString &def, QgsMeshMemoryDatasetGroup &datasetGroup ); - bool addDatasetValues( const QString &def, QgsMeshMemoryDataset &dataset, bool isScalar ); - bool checkDatasetValidity( QgsMeshMemoryDataset &dataset, bool isOnVertices ); + bool addDatasetValues( const QString &def, std::shared_ptr &dataset, bool isScalar ); + bool checkDatasetValidity( std::shared_ptr &dataset, bool isOnVertices ); QVector mVertices; QVector mFaces; diff --git a/src/core/mesh/qgstriangularmesh.h b/src/core/mesh/qgstriangularmesh.h index 4a3b6830b6db..17a985f9e579 100644 --- a/src/core/mesh/qgstriangularmesh.h +++ b/src/core/mesh/qgstriangularmesh.h @@ -143,13 +143,13 @@ class CORE_EXPORT QgsTriangularMesh namespace QgsMeshUtils { //! Returns face as polygon geometry - QgsGeometry toGeometry( const QgsMeshFace &face, const QVector &vertices ); + CORE_EXPORT QgsGeometry toGeometry( const QgsMeshFace &face, const QVector &vertices ); /** * Returns unique native faces indexes from list of triangle indexes * \since QGIS 3.4 */ - QList nativeFacesFromTriangles( const QList &triangleIndexes, const QVector &trianglesToNativeFaces ); + CORE_EXPORT QList nativeFacesFromTriangles( const QList &triangleIndexes, const QVector &trianglesToNativeFaces ); }; #endif // QGSTRIANGULARMESH_H diff --git a/src/providers/mdal/qgsmdalprovider.cpp b/src/providers/mdal/qgsmdalprovider.cpp index 455d89ea6b78..1e42d2c0dab0 100644 --- a/src/providers/mdal/qgsmdalprovider.cpp +++ b/src/providers/mdal/qgsmdalprovider.cpp @@ -20,6 +20,7 @@ #include "qgsmdalprovider.h" #include "qgstriangularmesh.h" #include "qgslogger.h" +#include "qgsmeshmemorydataprovider.h" #ifdef HAVE_GUI #include "qgssourceselectprovider.h" @@ -171,6 +172,75 @@ QgsRectangle QgsMdalProvider::extent() const return ret; } +bool QgsMdalProvider::persistDatasetGroup( const QString &path, + const QgsMeshDatasetGroupMetadata &meta, + const QVector &datasetValues, + const QVector &datasetActive, + const QVector × + ) +{ + if ( !mMeshH ) + return true; + + // Check that the input vectors have consistent size + if ( times.size() != datasetValues.size() ) + return true; + + if ( !datasetActive.isEmpty() && ( times.size() != datasetActive.size() ) ) + return true; + + // Check that input data are for all values + int valuesCount = meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices ? vertexCount() : faceCount(); + for ( int i = 0; i < datasetValues.size(); ++i ) + { + if ( datasetValues.at( i ).count() != valuesCount ) + return true; + + if ( !datasetActive.isEmpty() && ( datasetActive.at( i ).count() != faceCount() ) ) + return true; + } + + const QString driverName( "BINARY_DAT" ); //nothing else is implemented in MDAL + DriverH driver = MDAL_driverFromName( driverName.toStdString().c_str() ); + if ( !driver ) + return true; + + DatasetGroupH g = MDAL_M_addDatasetGroup( + mMeshH, + meta.name().toStdString().c_str(), + meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices, + meta.isScalar(), + driver, + path.toStdString().c_str() + ); + if ( !g ) + return true; + + auto end = meta.extraOptions().cend(); + for ( auto it = meta.extraOptions().cbegin(); it != end; ++it ) + { + MDAL_G_setMetadata( g, it.key().toStdString().c_str(), it.value().toStdString().c_str() ); + } + + + for ( int i = 0; i < datasetValues.size(); ++i ) + { + MDAL_G_addDataset( g, + times.at( i ), + static_cast( datasetValues.at( i ).constBuffer() ), + datasetActive.isEmpty() ? nullptr : static_cast( datasetActive.at( i ).constBuffer() ) + ); + } + + MDAL_G_closeEditMode( g ); + + emit datasetGroupsAdded( 1 ); + emit dataChanged(); + + return false; +} + + void QgsMdalProvider::fileMeshFilters( QString &fileMeshFiltersString, QString &fileMeshDatasetFiltersString ) { DriverH mdalDriver; diff --git a/src/providers/mdal/qgsmdalprovider.h b/src/providers/mdal/qgsmdalprovider.h index c7f94e19194f..4ff7738f94cf 100644 --- a/src/providers/mdal/qgsmdalprovider.h +++ b/src/providers/mdal/qgsmdalprovider.h @@ -28,6 +28,7 @@ class QMutex; class QgsCoordinateTransform; class QgsCoordinateReferenceSystem; +class QgsMeshMemoryDatasetGroup; /** \brief Data provider for MDAL layers. @@ -70,6 +71,13 @@ class QgsMdalProvider : public QgsMeshDataProvider QgsMeshDataBlock areFacesActive( QgsMeshDatasetIndex index, int faceIndex, int count ) const override; QgsRectangle extent() const override; + bool persistDatasetGroup( const QString &path, + const QgsMeshDatasetGroupMetadata &meta, + const QVector &datasetValues, + const QVector &datasetActive, + const QVector × + ) override; + /** * Returns file filters for meshes and datasets to be used in Open File Dialogs * \param fileMeshFiltersString file mesh filters diff --git a/src/ui/mesh/qgsmeshcalculatordialogbase.ui b/src/ui/mesh/qgsmeshcalculatordialogbase.ui new file mode 100644 index 000000000000..b87e1b79be57 --- /dev/null +++ b/src/ui/mesh/qgsmeshcalculatordialogbase.ui @@ -0,0 +1,649 @@ + + + QgsMeshCalculatorDialogBase + + + + 0 + 0 + 912 + 864 + + + + Mesh calculator + + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + Datasets + + + + + + QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + + + + + + Result layer + + + + + + + 0 + 0 + + + + + + + X min + + + + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + + + + + X max + + + + + + + Y max + + + + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + + + + + Y min + + + + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + + + + + Current layer extent + + + + + + + + + + + + + Current extent + + + true + + + buttonGroup + + + + + + + Mask layer + + + buttonGroup + + + + + + + + + + + 6 + + + 0 + + + 0 + + + + + + 206 + 0 + + + + Mask Layer + + + + + + + + 0 + 0 + + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Output dataset + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + All seletected dataset times + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + End time + + + + + + + Start time + + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + + + + maskBox + verticalSpacer_3 + verticalSpacer + extendBox + horizontalWidget + + + + + + + + Mesh calculator expression + + + + 0 + + + 0 + + + 0 + + + + + Operators + + + + 0 + + + 0 + + + 0 + + + + + <= + + + + + + + >= + + + + + + + / + + + + + + + * + + + + + + + + + + + + + + + max + + + + + + + = + + + + + + + > + + + + + + + < + + + + + + + abs + + + + + + + != + + + + + + + min + + + + + + + ( + + + + + + + ) + + + + + + + IF + + + + + + + AND + + + + + + + OR + + + + + + + NOT + + + + + + + ^ + + + + + + + sum (aggr) + + + + + + + max (aggr) + + + + + + + min (aggr) + + + + + + + average (aggr) + + + + + + + - + + + + + + + + + + NODATA + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + mButtonBox + mExpressionValidLabel + splitter_2 + + + + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsMapLayerComboBox + QComboBox +
qgsmaplayercombobox.h
+
+ + QgsFileWidget + QWidget +
qgsfilewidget.h
+
+
+ + mDatasetsListWidget + mXMinSpinBox + mXMaxSpinBox + mYMinSpinBox + mYMaxSpinBox + mPlusPushButton + mMultiplyPushButton + mMinusPushButton + mDividePushButton + mExpressionTextEdit + + + + + mButtonBox + accepted() + QgsMeshCalculatorDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + mButtonBox + rejected() + QgsMeshCalculatorDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + + +
diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index e55060b1186e..3f2d81cc6516 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -367,6 +367,12 @@ + + + &Mesh + + + @@ -375,6 +381,7 @@ + @@ -2984,7 +2991,7 @@ Acts on currently active editable layer true - + :/images/themes/default/mActionRectangle3PointsDistance.svg:/images/themes/default/mActionRectangle3PointsDistance.svg @@ -3135,7 +3142,7 @@ Acts on currently active editable layer true - + :/images/themes/default/mActionRectangle3PointsProjected.svg:/images/themes/default/mActionRectangle3PointsProjected.svg @@ -3145,9 +3152,49 @@ Acts on currently active editable layer Add rectangle from 3 points (Distance from projected point on segment p1 and p2) + + + + :/images/themes/default/mActionShowMeshCalculator.png:/images/themes/default/mActionShowMeshCalculator.png + + + Mesh Calculator… + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/qgsrastercalcdialogbase.ui b/src/ui/qgsrastercalcdialogbase.ui index c6d44340ec35..b31c3a38b95a 100644 --- a/src/ui/qgsrastercalcdialogbase.ui +++ b/src/ui/qgsrastercalcdialogbase.ui @@ -7,7 +7,7 @@ 0 0 756 - 637 + 644 @@ -84,7 +84,7 @@ - X Max + X max @@ -164,7 +164,7 @@ - + diff --git a/tests/src/analysis/CMakeLists.txt b/tests/src/analysis/CMakeLists.txt index 78fd98222983..9f57c65a94c5 100644 --- a/tests/src/analysis/CMakeLists.txt +++ b/tests/src/analysis/CMakeLists.txt @@ -23,6 +23,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src/analysis/processing ${CMAKE_SOURCE_DIR}/src/analysis/vector ${CMAKE_SOURCE_DIR}/src/analysis/raster + ${CMAKE_SOURCE_DIR}/src/analysis/mesh ${CMAKE_SOURCE_DIR}/src/test ${CMAKE_BINARY_DIR}/src/core @@ -81,8 +82,9 @@ SET(TESTS testqgsreclassifyutils.cpp testqgsalignraster.cpp testqgsnetworkanalysis.cpp - testqgsninecellfilters.cpp - ) + testqgsninecellfilters.cpp + testqgsmeshcalculator.cpp +) FOREACH(TESTSRC ${TESTS}) ADD_QGIS_TEST(${TESTSRC}) diff --git a/tests/src/analysis/testqgsmeshcalculator.cpp b/tests/src/analysis/testqgsmeshcalculator.cpp new file mode 100644 index 000000000000..0840a03645dd --- /dev/null +++ b/tests/src/analysis/testqgsmeshcalculator.cpp @@ -0,0 +1,218 @@ +/*************************************************************************** + testqgsmeshcalculator.cpp + -------------------------------------- +Date : December 2018 +Copyright : (C) 2018 by Peter Petrik +Email : zilolv at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgstest.h" + +#include "qgsmeshcalculator.h" +#include "qgsmeshcalcnode.h" +#include "qgsmeshdataprovider.h" +#include "qgsmeshlayer.h" +#include "qgsapplication.h" +#include "qgsproject.h" +#include "qgsmeshmemorydataprovider.h" + +Q_DECLARE_METATYPE( QgsMeshCalcNode::Operator ) + +class TestQgsMeshCalculator : public QObject +{ + Q_OBJECT + + public: + TestQgsMeshCalculator() = default; + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() ;// will be called before each testfunction is executed. + void cleanup() ;// will be called after every testfunction. + + void dualOp_data(); + void dualOp(); //test operators which operate on a left&right node + + void singleOp_data(); + void singleOp(); //test operators which operate on a single value + + void calcWithLayers(); + private: + + QgsMeshLayer *mpMeshLayer = nullptr; +}; + +void TestQgsMeshCalculator::initTestCase() +{ + // + // Runs once before any tests are run + // + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/mesh/" ); + QString uri( testDataDir + "/quad_and_triangle.2dm" ); + mpMeshLayer = new QgsMeshLayer( uri, "Triangle and Quad MDAL", "mdal" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar2.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar_max.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector2.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector_max.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_els_face_scalar.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_els_face_vector.dat" ); + + QgsProject::instance()->addMapLayers( + QList() << mpMeshLayer ); +} + +void TestQgsMeshCalculator::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsMeshCalculator::init() +{ +} +void TestQgsMeshCalculator::cleanup() +{ +} + +void TestQgsMeshCalculator::dualOp_data() +{ + QTest::addColumn< QgsMeshCalcNode::Operator >( "op" ); + QTest::addColumn( "left" ); + QTest::addColumn( "right" ); + QTest::addColumn( "expected" ); + + QTest::newRow( "opPlus" ) << QgsMeshCalcNode::opPLUS << 5.5 << 2.2 << 7.7; + QTest::newRow( "opMINUS" ) << QgsMeshCalcNode::opMINUS << 5.0 << 2.5 << 2.5; + QTest::newRow( "opMUL" ) << QgsMeshCalcNode::opMUL << 2.5 << 4.0 << 10.0; + QTest::newRow( "opDIV" ) << QgsMeshCalcNode::opDIV << 2.5 << 2.0 << 1.25; + QTest::newRow( "opDIV by 0" ) << QgsMeshCalcNode::opDIV << 2.5 << 0.0 << -9999.0; + QTest::newRow( "opPOW" ) << QgsMeshCalcNode::opPOW << 3.0 << 2.0 << 9.0; + QTest::newRow( "opPOW negative" ) << QgsMeshCalcNode::opPOW << 4.0 << -2.0 << 0.0625; + QTest::newRow( "opPOW sqrt" ) << QgsMeshCalcNode::opPOW << 4.0 << 0.5 << 2.0; + QTest::newRow( "opPOW complex" ) << QgsMeshCalcNode::opPOW << -2.0 << 0.5 << -9999.0; + QTest::newRow( "opEQ true" ) << QgsMeshCalcNode::opEQ << 1.0 << 1.0 << 1.0; + QTest::newRow( "opEQ false" ) << QgsMeshCalcNode::opEQ << 0.5 << 1.0 << 0.0; + QTest::newRow( "opNE equal" ) << QgsMeshCalcNode::opNE << 1.0 << 1.0 << 0.0; + QTest::newRow( "opNE not equal" ) << QgsMeshCalcNode::opNE << 0.5 << 1.0 << 1.0; + QTest::newRow( "opGT >" ) << QgsMeshCalcNode::opGT << 1.0 << 0.5 << 1.0; + QTest::newRow( "opGT =" ) << QgsMeshCalcNode::opGT << 0.5 << 0.5 << 0.0; + QTest::newRow( "opGT <" ) << QgsMeshCalcNode::opGT << 0.5 << 1.0 << 0.0; + QTest::newRow( "opLT >" ) << QgsMeshCalcNode::opLT << 1.0 << 0.5 << 0.0; + QTest::newRow( "opLT =" ) << QgsMeshCalcNode::opLT << 0.5 << 0.5 << 0.0; + QTest::newRow( "opLT <" ) << QgsMeshCalcNode::opLT << 0.5 << 1.0 << 1.0; + QTest::newRow( "opGE >" ) << QgsMeshCalcNode::opGE << 1.0 << 0.5 << 1.0; + QTest::newRow( "opGE =" ) << QgsMeshCalcNode::opGE << 0.5 << 0.5 << 1.0; + QTest::newRow( "opGE <" ) << QgsMeshCalcNode::opGE << 0.5 << 1.0 << 0.0; + QTest::newRow( "opLE >" ) << QgsMeshCalcNode::opLE << 1.0 << 0.5 << 0.0; + QTest::newRow( "opLE =" ) << QgsMeshCalcNode::opLE << 0.5 << 0.5 << 1.0; + QTest::newRow( "opLE <" ) << QgsMeshCalcNode::opLE << 0.5 << 1.0 << 1.0; + QTest::newRow( "opAND 0/0" ) << QgsMeshCalcNode::opAND << 0.0 << 0.0 << 0.0; + QTest::newRow( "opAND 0/1" ) << QgsMeshCalcNode::opAND << 0.0 << 1.0 << 0.0; + QTest::newRow( "opAND 1/0" ) << QgsMeshCalcNode::opAND << 1.0 << 0.0 << 0.0; + QTest::newRow( "opAND 1/1" ) << QgsMeshCalcNode::opAND << 1.0 << 1.0 << 1.0; + QTest::newRow( "opOR 0/0" ) << QgsMeshCalcNode::opOR << 0.0 << 0.0 << 0.0; + QTest::newRow( "opOR 0/1" ) << QgsMeshCalcNode::opOR << 0.0 << 1.0 << 1.0; + QTest::newRow( "opOR 1/0" ) << QgsMeshCalcNode::opOR << 1.0 << 0.0 << 1.0; + QTest::newRow( "opOR 1/1" ) << QgsMeshCalcNode::opOR << 1.0 << 1.0 << 1.0; +} + +void TestQgsMeshCalculator::dualOp() +{ + QFETCH( QgsMeshCalcNode::Operator, op ); + QFETCH( double, left ); + QFETCH( double, right ); + QFETCH( double, expected ); + + QgsMeshCalcNode node( op, new QgsMeshCalcNode( left ), new QgsMeshCalcNode( right ) ); + + QgsMeshMemoryDatasetGroup result( "result" ); + QStringList usedDatasetNames; + usedDatasetNames << "VertexScalarDataset2" << "VertexScalarDataset" << "VertexScalarDatasetMax"; + + QgsMeshCalcUtils utils( mpMeshLayer, + usedDatasetNames, + 0, + 3600 ); + + QVERIFY( node.calculate( utils, result ) ); + QCOMPARE( result.datasetCount(), 1 ); + std::shared_ptr ds = result.datasets[0]; + for ( int i = 0; i < ds->values.size(); ++i ) + QCOMPARE( ds->values.at( i ), expected ); +} + +void TestQgsMeshCalculator::singleOp_data() +{ + QTest::addColumn< QgsMeshCalcNode::Operator >( "op" ); + QTest::addColumn( "value" ); + QTest::addColumn( "expected" ); + + QTest::newRow( "opSIGN +" ) << QgsMeshCalcNode::opSIGN << 1.0 << -1.0; + QTest::newRow( "opSIGN -" ) << QgsMeshCalcNode::opSIGN << -1.0 << 1.0; + QTest::newRow( "opABS +" ) << QgsMeshCalcNode::opABS << 1.0 << 1.0; + QTest::newRow( "opABS -" ) << QgsMeshCalcNode::opABS << -1.0 << 1.0; +} + +void TestQgsMeshCalculator::singleOp() +{ + QFETCH( QgsMeshCalcNode::Operator, op ); + QFETCH( double, value ); + QFETCH( double, expected ); + + QgsMeshCalcNode node( op, new QgsMeshCalcNode( value ), nullptr ); + + QgsMeshMemoryDatasetGroup result( "result" ); + QStringList usedDatasetNames; + usedDatasetNames << "VertexScalarDataset"; + + QgsMeshCalcUtils utils( mpMeshLayer, + usedDatasetNames, + 0, + 3600 ); + + QVERIFY( node.calculate( utils, result ) ); + + QCOMPARE( result.datasetCount(), 1 ); + std::shared_ptr ds = result.datasets[0]; + for ( int i = 0; i < ds->values.size(); ++i ) + QCOMPARE( ds->values.at( i ), expected ); +} + +void TestQgsMeshCalculator::calcWithLayers() +{ + QgsCoordinateReferenceSystem crs; + crs.createFromId( 32633, QgsCoordinateReferenceSystem::EpsgCrsId ); + QgsRectangle extent( 783235, 3348110, 783350, 3347960 ); + + QTemporaryFile tmpFile; + tmpFile.open(); // fileName is no avialable until open + QString tmpName = tmpFile.fileName(); + tmpFile.close(); + + QgsMeshCalculator rc( QStringLiteral( "\"VertexScalarDataset\" + 2" ), + tmpName, + extent, + 0, + 3600, + mpMeshLayer + ); + int groupCount = mpMeshLayer->dataProvider()->datasetGroupCount(); + QCOMPARE( static_cast< int >( rc.processCalculation() ), 0 ); + int newGroupCount = mpMeshLayer->dataProvider()->datasetGroupCount(); + QCOMPARE( newGroupCount, groupCount + 1 ); +} + +QGSTEST_MAIN( TestQgsMeshCalculator ) +#include "testqgsmeshcalculator.moc" diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index aab3841bad19..37c1dd1b13dd 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -10,6 +10,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/layout ${CMAKE_SOURCE_DIR}/src/core/locator ${CMAKE_SOURCE_DIR}/src/core/metadata + ${CMAKE_SOURCE_DIR}/src/core/mesh ${CMAKE_SOURCE_DIR}/src/core/raster ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/ui @@ -21,9 +22,13 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/gui/raster ${CMAKE_SOURCE_DIR}/src/python ${CMAKE_SOURCE_DIR}/src/app + ${CMAKE_SOURCE_DIR}/src/app/mesh ${CMAKE_SOURCE_DIR}/src/app/pluginmanager ${CMAKE_SOURCE_DIR}/src/test + ${CMAKE_SOURCE_DIR}/src/analysis + ${CMAKE_SOURCE_DIR}/src/analysis/mesh + ${CMAKE_BINARY_DIR}/src/analysis ${CMAKE_BINARY_DIR}/src/core ${CMAKE_BINARY_DIR}/src/gui ${CMAKE_BINARY_DIR}/src/python @@ -113,4 +118,4 @@ ADD_QGIS_TEST(vectorlayersaveasdialogtest testqgsvectorlayersaveasdialog.cpp) ADD_QGIS_TEST(maptoolreverselinetest testqgsmaptoolreverseline.cpp) ADD_QGIS_TEST(maptooltrimextendfeaturetest testqgsmaptooltrimextendfeature.cpp) ADD_QGIS_TEST(projectproperties testqgsprojectproperties.cpp) - +ADD_QGIS_TEST(meshcalculator testqgsmeshcalculatordialog.cpp) diff --git a/tests/src/app/testqgsmeshcalculatordialog.cpp b/tests/src/app/testqgsmeshcalculatordialog.cpp new file mode 100644 index 000000000000..7ec40b9fcfce --- /dev/null +++ b/tests/src/app/testqgsmeshcalculatordialog.cpp @@ -0,0 +1,109 @@ +/*************************************************************************** + testqgsmeshcalculator.cpp + ------------------------- + Date : January 2019 + Copyright : (C) 2019 by Peter Petrik + Email : zilolv at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgstest.h" +#include "qgisapp.h" +#include "qgsapplication.h" +#include "qgsvectorlayer.h" +#include "qgsmeshlayer.h" +#include "qgsmeshdataprovider.h" +#include "qgsmeshcalculatordialog.h" +#include "qgsfeedback.h" + +#include + +/** + * \ingroup UnitTests + * This is a unit test for the mesh calculator + */ +class TestQgsMeshCalculatorDialog : public QObject +{ + Q_OBJECT + public: + TestQgsMeshCalculatorDialog(); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + + void testCalc(); + + private: + QgisApp *mQgisApp = nullptr; + QgsMeshLayer *mpMeshLayer = nullptr; +}; + +TestQgsMeshCalculatorDialog::TestQgsMeshCalculatorDialog() = default; + +//runs before all tests +void TestQgsMeshCalculatorDialog::initTestCase() +{ + qDebug() << "TestQgisAppClipboard::initTestCase()"; + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + mQgisApp = new QgisApp(); + + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/mesh/" ); + QString uri( testDataDir + "/quad_and_triangle.2dm" ); + mpMeshLayer = new QgsMeshLayer( uri, "Triangle and Quad MDAL", "mdal" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar2.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_scalar_max.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector2.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_vertex_vector_max.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_els_face_scalar.dat" ); + mpMeshLayer->dataProvider()->addDataset( testDataDir + "/quad_and_triangle_els_face_vector.dat" ); + + QgsProject::instance()->addMapLayers( + QList() << mpMeshLayer ); +} + +//runs after all tests +void TestQgsMeshCalculatorDialog::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsMeshCalculatorDialog::testCalc() +{ + std::unique_ptr< QgsMeshCalculatorDialog > dialog( new QgsMeshCalculatorDialog( mpMeshLayer ) ); + + int groupCount = mpMeshLayer->dataProvider()->datasetGroupCount(); + + QTemporaryFile tmpFile; + tmpFile.open(); // fileName is not available until open + QString tmpName = tmpFile.fileName(); + tmpFile.close(); + + // this next part is fragile, and may need to be modified if the dialog changes: + dialog->mOutputDatasetFileWidget->setFilePath( tmpName ); + dialog->mExpressionTextEdit->setText( QStringLiteral( "\"VertexScalarDataset\" * 2 " ) ); + dialog->accept(); + std::unique_ptr calculator = dialog->calculator(); + + QgsFeedback feedback; + QgsMeshCalculator::Result res = calculator->processCalculation( &feedback ); + QCOMPARE( res, QgsMeshCalculator::Success ); + + // check result + int newGroupCount = mpMeshLayer->dataProvider()->datasetGroupCount(); + QCOMPARE( groupCount + 1, newGroupCount ); +} + +QGSTEST_MAIN( TestQgsMeshCalculatorDialog ) +#include "testqgsmeshcalculatordialog.moc" diff --git a/tests/testdata/mesh/quad_and_triangle_vertex_scalar2.dat b/tests/testdata/mesh/quad_and_triangle_vertex_scalar2.dat new file mode 100644 index 000000000000..e3258e8489bd --- /dev/null +++ b/tests/testdata/mesh/quad_and_triangle_vertex_scalar2.dat @@ -0,0 +1,21 @@ +DATASET +OBJTYPE "mesh2d" +RT_JULIAN 2433282.500000 +BEGSCL +ND 5 +NC 2 +NAME "VertexScalarDataset2" +TIMEUNITS se +TS 0 0.000000 +11 +21 +31 +21 +11 +TS 0 3600.000000 +21 +31 +41 +31 +21 +ENDDS diff --git a/tests/testdata/mesh/quad_and_triangle_vertex_scalar_max.dat b/tests/testdata/mesh/quad_and_triangle_vertex_scalar_max.dat new file mode 100644 index 000000000000..92d0dc03be8d --- /dev/null +++ b/tests/testdata/mesh/quad_and_triangle_vertex_scalar_max.dat @@ -0,0 +1,15 @@ +DATASET +OBJTYPE "mesh2d" +RT_JULIAN 2433282.500000 +BEGSCL +ND 5 +NC 2 +NAME "VertexScalarDatasetMax" +TIMEUNITS se +TS 0 99999.0 +21 +31 +41 +31 +21 +ENDDS diff --git a/tests/testdata/mesh/quad_and_triangle_vertex_vector2.dat b/tests/testdata/mesh/quad_and_triangle_vertex_vector2.dat new file mode 100644 index 000000000000..1ced7be7b5d0 --- /dev/null +++ b/tests/testdata/mesh/quad_and_triangle_vertex_vector2.dat @@ -0,0 +1,21 @@ +DATASET +OBJTYPE "mesh2d" +RT_JULIAN 2433282.500000 +BEGVEC +ND 5 +NC 2 +NAME "VertexVectorDataset2" +TIMEUNITS se +TS 0 0.000000 +11 11 +21 11 +31 21 +21 21 +11 -21 +TS 0 3600.000000 +21 21 +31 21 +41 31 +31 31 +21 -11 +ENDDS diff --git a/tests/testdata/mesh/quad_and_triangle_vertex_vector_max.dat b/tests/testdata/mesh/quad_and_triangle_vertex_vector_max.dat new file mode 100644 index 000000000000..58876b514002 --- /dev/null +++ b/tests/testdata/mesh/quad_and_triangle_vertex_vector_max.dat @@ -0,0 +1,15 @@ +DATASET +OBJTYPE "mesh2d" +RT_JULIAN 2433282.500000 +BEGVEC +ND 5 +NC 2 +NAME "VertexVectorDatasetMax" +TIMEUNITS se +TS 0 99999.0 +1 1 +2 1 +3 2 +2 2 +1 -2 +ENDDS