diff --git a/python/core/raster/qgsrasterblock.sip b/python/core/raster/qgsrasterblock.sip index eb0442e6a0d..56e655929cf 100644 --- a/python/core/raster/qgsrasterblock.sip +++ b/python/core/raster/qgsrasterblock.sip @@ -205,6 +205,16 @@ class QgsRasterBlock */ QByteArray data() const; + /** Rewrite raw pixel data. + * If the data array is shorter than the internal array within the raster block object, + * pixels at the end will stay untouched. If the data array is longer than the internal + * array, only the initial data from the input array will be used. + * Optionally it is possible to set non-zero offset (in bytes) if the input data should + * overwrite data somewhere in the middle of the internal buffer. + * @note added in QGIS 3.0 + */ + void setData( const QByteArray& data, int offset = 0 ); + /** \brief Get pointer to data * @param row row index * @param column column index diff --git a/python/core/raster/qgsrasterdataprovider.sip b/python/core/raster/qgsrasterdataprovider.sip index 27c142bb368..80c937aa754 100644 --- a/python/core/raster/qgsrasterdataprovider.sip +++ b/python/core/raster/qgsrasterdataprovider.sip @@ -233,10 +233,44 @@ class QgsRasterDataProvider : QgsDataProvider, QgsRasterInterface /** Current time stamp of data source */ virtual QDateTime dataTimestamp() const; + /** Checks whether the provider is in editing mode, i.e. raster write operations will be accepted. + * By default providers are not editable. Use setEditable() method to enable/disable editing. + * @see setEditable(), writeBlock() + * @note added in QGIS 3.0 + */ + virtual bool isEditable() const; + + /** Turns on/off editing mode of the provider. When in editing mode, it is possible + * to overwrite data of the provider using writeBlock() calls. + * @note Only some providers support editing mode and even those may fail to turn turn + * the underlying data source into editing mode, so it is necessery to check afterwards + * with isEditable() whether the operation was successful. + * @see isEditable(), writeBlock() + * @note added in QGIS 3.0 + */ + virtual void setEditable( bool enabled ); + /** Writes into the provider datasource*/ // TODO: add data type (may be defferent from band type) virtual bool write( void* data, int band, int width, int height, int xOffset, int yOffset ); + /** Writes pixel data from a raster block into the provider data source. + * + * This will override previously stored pixel values. It is assumed that cells in the passed + * raster block are aligned with the cells of the data source. If raster block does not cover + * the whole area of the data source, only a subset of pixels covered by the raster block + * will be overwritten. By default, writing of raster data starts from the first cell + * of the raster - it is possible to set offset in pixels by specifying non-zero + * xOffset and yOffset values. + * + * Writing is supported only by some data providers. Provider has to be in editing mode + * in order to allow write operations. + * @see isEditable(), setEditable() + * @returns true on success + * @note added in QGIS 3.0 + */ + bool writeBlock( QgsRasterBlock* block, int band, int xOffset = 0, int yOffset = 0 ); + /** Creates a new dataset with mDataSourceURI */ static QgsRasterDataProvider* create( const QString &providerKey, const QString &uri, diff --git a/src/core/raster/qgsrasterblock.cpp b/src/core/raster/qgsrasterblock.cpp index 69034cf86f7..f3ecf51c933 100644 --- a/src/core/raster/qgsrasterblock.cpp +++ b/src/core/raster/qgsrasterblock.cpp @@ -653,6 +653,23 @@ QByteArray QgsRasterBlock::data() const return QByteArray(); } +void QgsRasterBlock::setData( const QByteArray& data, int offset ) +{ + if ( offset < 0 ) + return; // negative offsets not allowed + + if ( mData ) + { + int len = qMin( data.size(), typeSize( mDataType ) * mWidth * mHeight - offset ); + ::memcpy( static_cast( mData ) + offset, data.constData(), len ); + } + else if ( mImage && mImage->constBits() ) + { + int len = qMin( data.size(), mImage->byteCount() - offset ); + ::memcpy( mImage->bits() + offset, data.constData(), len ); + } +} + char * QgsRasterBlock::bits( qgssize index ) { // Not testing type to avoid too much overhead because this method is called per pixel diff --git a/src/core/raster/qgsrasterblock.h b/src/core/raster/qgsrasterblock.h index 5341d54803c..3d1e66af1e4 100644 --- a/src/core/raster/qgsrasterblock.h +++ b/src/core/raster/qgsrasterblock.h @@ -270,6 +270,16 @@ class CORE_EXPORT QgsRasterBlock */ QByteArray data() const; + /** Rewrite raw pixel data. + * If the data array is shorter than the internal array within the raster block object, + * pixels at the end will stay untouched. If the data array is longer than the internal + * array, only the initial data from the input array will be used. + * Optionally it is possible to set non-zero offset (in bytes) if the input data should + * overwrite data somewhere in the middle of the internal buffer. + * @note added in QGIS 3.0 + */ + void setData( const QByteArray& data, int offset = 0 ); + /** \brief Get pointer to data * @param row row index * @param column column index diff --git a/src/core/raster/qgsrasterdataprovider.cpp b/src/core/raster/qgsrasterdataprovider.cpp index 785bd4dee41..2d087a36615 100644 --- a/src/core/raster/qgsrasterdataprovider.cpp +++ b/src/core/raster/qgsrasterdataprovider.cpp @@ -339,6 +339,18 @@ QString QgsRasterDataProvider::lastErrorFormat() return QStringLiteral( "text/plain" ); } +bool QgsRasterDataProvider::writeBlock( QgsRasterBlock* block, int band, int xOffset, int yOffset ) +{ + if ( !block ) + return false; + if ( !isEditable() ) + { + QgsDebugMsg( "writeBlock() called on read-only provider." ); + return false; + } + return write( block->bits(), band, block->width(), block->height(), xOffset, yOffset ); +} + typedef QList > *pyramidResamplingMethods_t(); QList > QgsRasterDataProvider::pyramidResamplingMethods( const QString& providerKey ) { diff --git a/src/core/raster/qgsrasterdataprovider.h b/src/core/raster/qgsrasterdataprovider.h index 82e5fe6c3e6..a5eb0590000 100644 --- a/src/core/raster/qgsrasterdataprovider.h +++ b/src/core/raster/qgsrasterdataprovider.h @@ -351,6 +351,23 @@ class CORE_EXPORT QgsRasterDataProvider : public QgsDataProvider, public QgsRast //! Current time stamp of data source virtual QDateTime dataTimestamp() const override { return QDateTime(); } + /** Checks whether the provider is in editing mode, i.e. raster write operations will be accepted. + * By default providers are not editable. Use setEditable() method to enable/disable editing. + * @see setEditable(), writeBlock() + * @note added in QGIS 3.0 + */ + virtual bool isEditable() const { return false; } + + /** Turns on/off editing mode of the provider. When in editing mode, it is possible + * to overwrite data of the provider using writeBlock() calls. + * @note Only some providers support editing mode and even those may fail to turn turn + * the underlying data source into editing mode, so it is necessery to check afterwards + * with isEditable() whether the operation was successful. + * @see isEditable(), writeBlock() + * @note added in QGIS 3.0 + */ + virtual void setEditable( bool enabled ) { Q_UNUSED( enabled ); } + //! Writes into the provider datasource // TODO: add data type (may be defferent from band type) virtual bool write( void* data, int band, int width, int height, int xOffset, int yOffset ) @@ -364,6 +381,23 @@ class CORE_EXPORT QgsRasterDataProvider : public QgsDataProvider, public QgsRast return false; } + /** Writes pixel data from a raster block into the provider data source. + * + * This will override previously stored pixel values. It is assumed that cells in the passed + * raster block are aligned with the cells of the data source. If raster block does not cover + * the whole area of the data source, only a subset of pixels covered by the raster block + * will be overwritten. By default, writing of raster data starts from the first cell + * of the raster - it is possible to set offset in pixels by specifying non-zero + * xOffset and yOffset values. + * + * Writing is supported only by some data providers. Provider has to be in editing mode + * in order to allow write operations. + * @see isEditable(), setEditable() + * @returns true on success + * @note added in QGIS 3.0 + */ + bool writeBlock( QgsRasterBlock* block, int band, int xOffset = 0, int yOffset = 0 ); + //! Creates a new dataset with mDataSourceURI static QgsRasterDataProvider* create( const QString &providerKey, const QString &uri, diff --git a/src/providers/gdal/qgsgdalprovider.cpp b/src/providers/gdal/qgsgdalprovider.cpp index 249ea878d92..0d18a72e697 100644 --- a/src/providers/gdal/qgsgdalprovider.cpp +++ b/src/providers/gdal/qgsgdalprovider.cpp @@ -2928,6 +2928,40 @@ QString QgsGdalProvider::validatePyramidsConfigOptions( QgsRaster::RasterPyramid return QString(); } +bool QgsGdalProvider::isEditable() const +{ + return mUpdate; +} + +void QgsGdalProvider::setEditable( bool enabled ) +{ + if ( enabled == mUpdate ) + return; + + if ( !mValid ) + return; + + if ( mGdalDataset != mGdalBaseDataset ) + return; // ignore the case of warped VRT for now (more complicated setup) + + closeDataset(); + + mUpdate = enabled; + + // reopen the dataset + mGdalBaseDataset = gdalOpen( dataSourceUri().toUtf8().constData(), mUpdate ? GA_Update : GA_ReadOnly ); + if ( !mGdalBaseDataset ) + { + QString msg = QStringLiteral( "Cannot reopen GDAL dataset %1:\n%2" ).arg( dataSourceUri(), QString::fromUtf8( CPLGetLastErrorMsg() ) ); + appendError( ERRMSG( msg ) ); + return; + } + + //Since we are not a virtual warped dataset, mGdalDataSet and mGdalBaseDataset are supposed to be the same + mGdalDataset = mGdalBaseDataset; + mValid = true; +} + // pyramids resampling // see http://www.gdal.org/gdaladdo.html diff --git a/src/providers/gdal/qgsgdalprovider.h b/src/providers/gdal/qgsgdalprovider.h index b1f58617c05..58a9b5971a0 100644 --- a/src/providers/gdal/qgsgdalprovider.h +++ b/src/providers/gdal/qgsgdalprovider.h @@ -148,14 +148,16 @@ class QgsGdalProvider : public QgsRasterDataProvider, QgsGdalProviderBase static QMap supportedMimes(); + bool isEditable() const override; + void setEditable( bool enabled ) override; bool write( void* data, int band, int width, int height, int xOffset, int yOffset ) override; + bool setNoDataValue( int bandNo, double noDataValue ) override; bool remove() override; QString validateCreationOptions( const QStringList& createOptions, const QString& format ) override; QString validatePyramidsConfigOptions( QgsRaster::RasterPyramidsFormat pyramidsFormat, const QStringList & theConfigOptions, const QString & fileFormat ) override; - private: // update mode bool mUpdate; diff --git a/tests/src/core/testqgsrasterblock.cpp b/tests/src/core/testqgsrasterblock.cpp index 108538ceed6..a8bf98515c3 100644 --- a/tests/src/core/testqgsrasterblock.cpp +++ b/tests/src/core/testqgsrasterblock.cpp @@ -16,6 +16,7 @@ #include "qgstest.h" #include #include +#include #include "qgsrasterlayer.h" #include "qgsrasterdataprovider.h" @@ -41,6 +42,7 @@ class TestQgsRasterBlock : public QObject void cleanup() {} // will be called after every testfunction. void testBasic(); + void testWrite(); private: @@ -116,6 +118,95 @@ void TestQgsRasterBlock::testBasic() QCOMPARE( data.at( 1 ), ( char ) 5 ); QCOMPARE( data.at( 10 ), ( char ) 27 ); + // setData() + QByteArray newData( "\xaa\xbb\xcc\xdd" ); + block->setData( newData, 1 ); + QByteArray data2 = block->data(); + QCOMPARE( data2.at( 0 ), ( char ) 2 ); + QCOMPARE( data2.at( 1 ), '\xaa' ); + QCOMPARE( data2.at( 2 ), '\xbb' ); + QCOMPARE( data2.at( 10 ), ( char ) 27 ); + + delete block; +} + +void TestQgsRasterBlock::testWrite() +{ + QgsRectangle extent = mpRasterLayer->extent(); + int nCols = mpRasterLayer->width(), nRows = mpRasterLayer->height(); + double tform[] = + { + extent.xMinimum(), extent.width() / nCols, 0.0, + extent.yMaximum(), 0.0, -extent.height() / nRows + }; + + // generate unique filename (need to open the file first to generate it) + QTemporaryFile tmpFile; + tmpFile.open(); + tmpFile.close(); + + // create a GeoTIFF - this will create data provider in editable mode + QString filename = tmpFile.fileName(); + QgsRasterDataProvider* dp = QgsRasterDataProvider::create( "gdal", filename, "GTiff", 1, Qgis::Byte, 10, 10, tform, mpRasterLayer->crs() ); + + QgsRasterBlock* block = mpRasterLayer->dataProvider()->block( 1, mpRasterLayer->extent(), mpRasterLayer->width(), mpRasterLayer->height() ); + + QByteArray origData = block->data(); + origData.detach(); // make sure we have private copy independent from independent block content + QCOMPARE( origData.at( 0 ), ( char ) 2 ); + QCOMPARE( origData.at( 1 ), ( char ) 5 ); + + // change first two pixels + block->setData( QByteArray( "\xa0\xa1" ) ); + bool res = dp->writeBlock( block, 1 ); + QVERIFY( res ); + + QgsRasterBlock* block2 = dp->block( 1, mpRasterLayer->extent(), mpRasterLayer->width(), mpRasterLayer->height() ); + QByteArray newData2 = block2->data(); + QCOMPARE( newData2.at( 0 ), '\xa0' ); + QCOMPARE( newData2.at( 1 ), '\xa1' ); + + delete block2; + delete dp; + + // newly open raster and verify the write was permanent + QgsRasterLayer* rlayer = new QgsRasterLayer( filename, "tmp", "gdal" ); + QVERIFY( rlayer->isValid() ); + QgsRasterBlock* block3 = rlayer->dataProvider()->block( 1, rlayer->extent(), rlayer->width(), rlayer->height() ); + QByteArray newData3 = block3->data(); + QCOMPARE( newData3.at( 0 ), '\xa0' ); + QCOMPARE( newData3.at( 1 ), '\xa1' ); + + QgsRasterBlock* block4 = new QgsRasterBlock( Qgis::Byte, 1, 2 ); + block4->setData( QByteArray( "\xb0\xb1" ) ); + + // cannot write when provider is not editable + res = rlayer->dataProvider()->writeBlock( block4, 1 ); + QVERIFY( !res ); + + // make the provider editable + QVERIFY( !rlayer->dataProvider()->isEditable() ); + rlayer->dataProvider()->setEditable( true ); + QVERIFY( rlayer->dataProvider()->isEditable() ); + + res = rlayer->dataProvider()->writeBlock( block4, 1 ); + QVERIFY( res ); + + rlayer->dataProvider()->setEditable( false ); + QVERIFY( !rlayer->dataProvider()->isEditable() ); + + // verify the change is there + QgsRasterBlock* block5 = rlayer->dataProvider()->block( 1, rlayer->extent(), rlayer->width(), rlayer->height() ); + QByteArray newData5 = block5->data(); + QCOMPARE( newData5.at( 0 ), '\xb0' ); + QCOMPARE( newData5.at( 1 ), '\xa1' ); // original data + QCOMPARE( newData5.at( 10 ), '\xb1' ); + + delete block3; + delete block4; + delete block5; + delete rlayer; + delete block; }