Skip to content

Commit

Permalink
[FEATURE] API to enable/disable editing of raster data
Browse files Browse the repository at this point in the history
To create a 2x2 raster block with one byte per pixel:

```
block = QgsRasterBlock(Qgis.Byte, 2, 2)
block.setData(b'\xaa\xbb\xcc\xdd')
```

To overwrite existing raster data at position 0,0 by the 2x2 block:

```
provider.setEditable(True)
provider.writeBlock(block, band, 0, 0)
provider.setEditable(False)
```
  • Loading branch information
wonder-sk committed Jan 19, 2017
1 parent f5e4c9d commit f6f6ebd
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 1 deletion.
10 changes: 10 additions & 0 deletions python/core/raster/qgsrasterblock.sip
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions python/core/raster/qgsrasterdataprovider.sip
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/core/raster/qgsrasterblock.cpp
Expand Up @@ -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<char *>( 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
Expand Down
10 changes: 10 additions & 0 deletions src/core/raster/qgsrasterblock.h
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/core/raster/qgsrasterdataprovider.cpp
Expand Up @@ -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<QPair<QString, QString> > *pyramidResamplingMethods_t();
QList<QPair<QString, QString> > QgsRasterDataProvider::pyramidResamplingMethods( const QString& providerKey )
{
Expand Down
34 changes: 34 additions & 0 deletions src/core/raster/qgsrasterdataprovider.h
Expand Up @@ -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 )
Expand All @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions src/providers/gdal/qgsgdalprovider.cpp
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/providers/gdal/qgsgdalprovider.h
Expand Up @@ -148,14 +148,16 @@ class QgsGdalProvider : public QgsRasterDataProvider, QgsGdalProviderBase

static QMap<QString, QString> 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;
Expand Down
91 changes: 91 additions & 0 deletions tests/src/core/testqgsrasterblock.cpp
Expand Up @@ -16,6 +16,7 @@
#include "qgstest.h"
#include <QObject>
#include <QString>
#include <QTemporaryFile>

#include "qgsrasterlayer.h"
#include "qgsrasterdataprovider.h"
Expand All @@ -41,6 +42,7 @@ class TestQgsRasterBlock : public QObject
void cleanup() {} // will be called after every testfunction.

void testBasic();
void testWrite();

private:

Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit f6f6ebd

Please sign in to comment.