Refactor QgsPalettedRasterRenderer to allow "gaps" in pixel values

Previously the renderer required that pixel values followed
sequential numerical order. This refactor allows values to
be skipped, so that only certain color index will be rendered.
This commit is contained in:
Nyall Dawson 2017-03-23 11:17:20 +10:00
parent 620a8394c9
commit a157db7627
9 changed files with 232 additions and 156 deletions

View File

@ -1536,7 +1536,10 @@ QgsPaintEffectRegistry {#qgis_api_break_3_0_QgsPaintEffectRegistry}
QgsPalettedRasterRenderer {#qgis_api_break_3_0_QgsPalettedRasterRenderer}
-------------------------
- The rgbArray() method was made private
- The rgbArray() method was removed
- colors() has been removed, use classes() instead.
- The constructor has a different signature and requires a list of classes instead of an array
QgsPalLabeling {#qgis_api_break_3_0_QgsPalLabeling}
--------------

View File

@ -4,8 +4,17 @@ class QgsPalettedRasterRenderer : QgsRasterRenderer
#include "qgspalettedrasterrenderer.h"
%End
public:
/** Renderer owns color array*/
QgsPalettedRasterRenderer( QgsRasterInterface* input, int bandNumber, QColor* colorArray /Array,Transfer/, int nColors /ArraySize/, const QVector<QString>& labels = QVector<QString>() );
struct Class
{
Class( const QColor &color, const QString &label );
QColor color;
QString label;
};
typedef QMap< int, QgsPalettedRasterRenderer::Class > ClassData;
QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, const ClassData &classes );
~QgsPalettedRasterRenderer();
virtual QgsPalettedRasterRenderer * clone() const /Factory/;
static QgsRasterRenderer* create( const QDomElement& elem, QgsRasterInterface* input ) /Factory/;
@ -14,20 +23,13 @@ class QgsPalettedRasterRenderer : QgsRasterRenderer
/** Returns number of colors*/
int nColors() const;
/** Returns copy of color array (caller takes ownership)*/
QColor* colors() const /Factory/;
/** Return optional category label
* @note added in 2.1 */
ClassData classes() const;
QString label( int idx ) const;
void setLabel( int idx, const QString &label );
/** Set category label
* @note added in 2.1 */
void setLabel( int idx, const QString& label );
void writeXml( QDomDocument &doc, QDomElement &parentElem ) const;
void writeXml( QDomDocument& doc, QDomElement& parentElem ) const;
void legendSymbologyItems( QList< QPair< QString, QColor > >& symbolItems /Out/ ) const;
void legendSymbologyItems( QList< QPair< QString, QColor > > &symbolItems /Out/ ) const;
QList<int> usesBands() const;

View File

@ -26,34 +26,24 @@
#include <QVector>
#include <memory>
QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber,
QColor *colorArray, int nColors, const QVector<QString> &labels ):
QgsRasterRenderer( input, QStringLiteral( "paletted" ) ), mBand( bandNumber ), mNColors( nColors ), mLabels( labels )
{
mColors = new QRgb[nColors];
for ( int i = 0; i < nColors; ++i )
{
mColors[i] = qPremultiply( colorArray[i].rgba() );
}
delete[] colorArray;
}
QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, QRgb *colorArray, int nColors, const QVector<QString> &labels ):
QgsRasterRenderer( input, QStringLiteral( "paletted" ) ), mBand( bandNumber ), mColors( colorArray ), mNColors( nColors ), mLabels( labels )
QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, const ClassData &classes )
: QgsRasterRenderer( input, QStringLiteral( "paletted" ) )
, mBand( bandNumber )
, mClassData( classes )
{
updateArrays();
}
QgsPalettedRasterRenderer::~QgsPalettedRasterRenderer()
{
delete[] mColors;
delete[] mIsNoData;
}
QgsPalettedRasterRenderer *QgsPalettedRasterRenderer::clone() const
{
QgsPalettedRasterRenderer *renderer = new QgsPalettedRasterRenderer( nullptr, mBand, rgbArray(), mNColors );
QgsPalettedRasterRenderer *renderer = new QgsPalettedRasterRenderer( nullptr, mBand, mClassData );
renderer->copyCommonProperties( this );
renderer->mLabels = mLabels;
return renderer;
}
@ -65,9 +55,7 @@ QgsRasterRenderer *QgsPalettedRasterRenderer::create( const QDomElement &elem, Q
}
int bandNumber = elem.attribute( QStringLiteral( "band" ), QStringLiteral( "-1" ) ).toInt();
int nColors = 0;
QRgb *colors = nullptr;
QVector<QString> labels;
ClassData classData;
QDomElement paletteElem = elem.firstChildElement( QStringLiteral( "colorPalette" ) );
if ( !paletteElem.isNull() )
@ -76,89 +64,50 @@ QgsRasterRenderer *QgsPalettedRasterRenderer::create( const QDomElement &elem, Q
QDomElement entryElem;
int value;
nColors = 0;
// We cannot believe that data are correct, check first max value
for ( int i = 0; i < paletteEntries.size(); ++i )
{
entryElem = paletteEntries.at( i ).toElement();
// Could be written as doubles (with .0000) in old project files
value = ( int )entryElem.attribute( QStringLiteral( "value" ), QStringLiteral( "0" ) ).toDouble();
if ( value >= nColors && value <= 10000 ) nColors = value + 1;
}
QgsDebugMsgLevel( QString( "nColors = %1" ).arg( nColors ), 4 );
colors = new QRgb[ nColors ];
for ( int i = 0; i < nColors; ++i )
{
QColor color;
QString label;
entryElem = paletteEntries.at( i ).toElement();
value = ( int )entryElem.attribute( QStringLiteral( "value" ), QStringLiteral( "0" ) ).toDouble();
QgsDebugMsgLevel( entryElem.attribute( "color", "#000000" ), 4 );
if ( value >= 0 && value < nColors )
{
QColor color = QColor( entryElem.attribute( QStringLiteral( "color" ), QStringLiteral( "#000000" ) ) );
color = QColor( entryElem.attribute( QStringLiteral( "color" ), QStringLiteral( "#000000" ) ) );
color.setAlpha( entryElem.attribute( QStringLiteral( "alpha" ), QStringLiteral( "255" ) ).toInt() );
colors[value] = qPremultiply( color.rgba() );
QString label = entryElem.attribute( QStringLiteral( "label" ) );
if ( !label.isEmpty() )
{
if ( value >= labels.size() ) labels.resize( value + 1 );
labels[value] = label;
label = entryElem.attribute( QStringLiteral( "label" ) );
classData.insert( value, Class( color, label ) );
}
}
else
{
QgsDebugMsg( QString( "value %1 out of range" ).arg( value ) );
}
}
}
QgsPalettedRasterRenderer *r = new QgsPalettedRasterRenderer( input, bandNumber, colors, nColors, labels );
QgsPalettedRasterRenderer *r = new QgsPalettedRasterRenderer( input, bandNumber, classData );
r->readXml( elem );
return r;
}
QColor *QgsPalettedRasterRenderer::colors() const
QgsPalettedRasterRenderer::ClassData QgsPalettedRasterRenderer::classes() const
{
if ( mNColors < 1 )
{
return nullptr;
}
QColor *colorArray = new QColor[ mNColors ];
for ( int i = 0; i < mNColors; ++i )
{
colorArray[i] = QColor::fromRgba( qUnpremultiply( mColors[i] ) );
}
return colorArray;
return mClassData;
}
QRgb *QgsPalettedRasterRenderer::rgbArray() const
QString QgsPalettedRasterRenderer::label( int idx ) const
{
if ( mNColors < 1 )
{
return nullptr;
}
QRgb *rgbValues = new QRgb[mNColors];
for ( int i = 0; i < mNColors; ++i )
{
rgbValues[i] = mColors[i];
}
return rgbValues;
if ( !mClassData.contains( idx ) )
return QString();
return mClassData.value( idx ).label;
}
void QgsPalettedRasterRenderer::setLabel( int idx, const QString &label )
{
if ( idx >= mLabels.size() )
{
mLabels.resize( idx + 1 );
}
mLabels[idx] = label;
if ( !mClassData.contains( idx ) )
return;
mClassData[ idx ].label = label;
}
QgsRasterBlock *QgsPalettedRasterRenderer::block( int bandNo, QgsRectangle const &extent, int width, int height, QgsRasterBlockFeedback *feedback )
{
std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() );
if ( !mInput || mNColors == 0 )
if ( !mInput || mClassData.isEmpty() )
{
return outputBlock.release();
}
@ -211,6 +160,11 @@ QgsRasterBlock *QgsPalettedRasterRenderer::block( int bandNo, QgsRectangle cons
continue;
}
int val = ( int ) inputBlock->value( i );
if ( val > mMaxColorIndex || mIsNoData[ val ] )
{
outputData[i] = myDefaultColor;
continue;
}
if ( !hasTransparency )
{
@ -248,16 +202,17 @@ void QgsPalettedRasterRenderer::writeXml( QDomDocument &doc, QDomElement &parent
rasterRendererElem.setAttribute( QStringLiteral( "band" ), mBand );
QDomElement colorPaletteElem = doc.createElement( QStringLiteral( "colorPalette" ) );
for ( int i = 0; i < mNColors; ++i )
ClassData::const_iterator it = mClassData.constBegin();
for ( ; it != mClassData.constEnd(); ++it )
{
QColor color = QColor::fromRgba( qUnpremultiply( mColors[i] ) );
QColor color = it.value().color;
QDomElement colorElem = doc.createElement( QStringLiteral( "paletteEntry" ) );
colorElem.setAttribute( QStringLiteral( "value" ), i );
colorElem.setAttribute( QStringLiteral( "value" ), it.key() );
colorElem.setAttribute( QStringLiteral( "color" ), color.name() );
colorElem.setAttribute( QStringLiteral( "alpha" ), color.alpha() );
if ( !label( i ).isEmpty() )
if ( !it.value().label.isEmpty() )
{
colorElem.setAttribute( QStringLiteral( "label" ), label( i ) );
colorElem.setAttribute( QStringLiteral( "label" ), it.value().label );
}
colorPaletteElem.appendChild( colorElem );
}
@ -268,10 +223,25 @@ void QgsPalettedRasterRenderer::writeXml( QDomDocument &doc, QDomElement &parent
void QgsPalettedRasterRenderer::legendSymbologyItems( QList< QPair< QString, QColor > > &symbolItems ) const
{
for ( int i = 0; i < mNColors; ++i )
QList< QPair< int, QPair< QString, QColor > > > legends;
ClassData::const_iterator it = mClassData.constBegin();
for ( ; it != mClassData.constEnd(); ++it )
{
QString lab = label( i ).isEmpty() ? QString::number( i ) : label( i );
symbolItems.push_back( qMakePair( lab, QColor::fromRgba( qUnpremultiply( mColors[i] ) ) ) );
QString lab = it.value().label.isEmpty() ? QString::number( it.key() ) : it.value().label;
legends.push_back( qMakePair( it.key(), qMakePair( lab, it.value().color ) ) );
}
// sort by color value
std::sort( legends.begin(), legends.end(),
[]( const QPair< int, QPair< QString, QColor > > &a, const QPair< int, QPair< QString, QColor > > &b ) -> bool
{
return a.first < b.first;
} );
QList< QPair< int, QPair< QString, QColor > > >::const_iterator lIt = legends.constBegin();
for ( ; lIt != legends.constEnd(); ++lIt )
{
symbolItems << lIt->second;
}
}
@ -284,3 +254,28 @@ QList<int> QgsPalettedRasterRenderer::usesBands() const
}
return bandList;
}
void QgsPalettedRasterRenderer::updateArrays()
{
// find maximum color index
ClassData::const_iterator mIt = mClassData.constBegin();
for ( ; mIt != mClassData.constEnd(); ++mIt )
{
mMaxColorIndex = qMax( mMaxColorIndex, mIt.key() );
}
delete [] mColors;
delete [] mIsNoData;
mColors = new QRgb[mMaxColorIndex + 1];
mIsNoData = new bool[mMaxColorIndex + 1];
std::fill( mIsNoData, mIsNoData + mMaxColorIndex, true );
int i = 0;
ClassData::const_iterator it = mClassData.constBegin();
for ( ; it != mClassData.constEnd(); ++it )
{
mColors[it.key()] = qPremultiply( it.value().color.rgba() );
mIsNoData[it.key()] = false;
i++;
}
}

View File

@ -33,9 +33,28 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
{
public:
//! Renderer owns color array
QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, QColor *colorArray, int nColors, const QVector<QString> &labels = QVector<QString>() );
QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, QRgb *colorArray, int nColors, const QVector<QString> &labels = QVector<QString>() );
//! Properties of a single value class
struct Class
{
//! Constructor for Class
Class( const QColor &color = QColor(), const QString &label = QString() )
: color( color )
, label( label )
{}
//! Color to render value
QColor color;
//! Label for value
QString label;
};
//! Map of value to class properties
typedef QMap< int, Class > ClassData;
/**
* Constructor for QgsPalettedRasterRenderer.
*/
QgsPalettedRasterRenderer( QgsRasterInterface *input, int bandNumber, const ClassData &classes );
~QgsPalettedRasterRenderer();
//! QgsPalettedRasterRenderer cannot be copied. Use clone() instead.
@ -49,13 +68,16 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override;
//! Returns number of colors
int nColors() const { return mNColors; }
//! Returns copy of color array (caller takes ownership)
QColor *colors() const;
int nColors() const { return mClassData.size(); }
/**
* Returns a map of value to classes (colors) used by the renderer.
*/
ClassData classes() const;
/** Return optional category label
* \since QGIS 2.1 */
QString label( int idx ) const { return mLabels.value( idx ); }
QString label( int idx ) const;
/** Set category label
* \since QGIS 2.1 */
@ -69,17 +91,16 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer
private:
/** Returns copy of premultiplied rgb array (caller takes ownership)
*/
QRgb *rgbArray() const;
int mBand;
int mMaxColorIndex = -INT_MAX;
ClassData mClassData;
//! Premultiplied color array
QRgb *mColors = nullptr;
//! Number of colors
int mNColors;
//! Optional category labels, size of vector may be < mNColors
QVector<QString> mLabels;
bool *mIsNoData = nullptr;
void updateArrays();
};

View File

@ -124,37 +124,17 @@ QgsRasterRenderer *QgsRasterRendererRegistry::defaultRendererForDrawingStyle( Qg
int grayBand = 1; //reasonable default
QList<QgsColorRampShader::ColorRampItem> colorEntries = provider->colorTable( grayBand );
//go through list and take maximum value (it could be that entries don't start at 0 or indices are not contiguous)
int colorArraySize = 0;
QList<QgsColorRampShader::ColorRampItem>::const_iterator colorIt = colorEntries.constBegin();
for ( ; colorIt != colorEntries.constEnd(); ++colorIt )
{
if ( colorIt->value > colorArraySize )
{
colorArraySize = ( int )( colorIt->value );
}
}
colorArraySize += 1; //usually starts at 0
QColor *colorArray = new QColor[ colorArraySize ];
colorIt = colorEntries.constBegin();
QVector<QString> labels;
QgsPalettedRasterRenderer::ClassData classes;
for ( ; colorIt != colorEntries.constEnd(); ++colorIt )
{
int idx = ( int )( colorIt->value );
colorArray[idx] = colorIt->color;
if ( !colorIt->label.isEmpty() )
{
if ( labels.size() <= idx ) labels.resize( idx + 1 );
labels[idx] = colorIt->label;
}
classes.insert( idx, QgsPalettedRasterRenderer::Class( colorIt->color, colorIt->label ) );
}
renderer = new QgsPalettedRasterRenderer( provider,
grayBand,
colorArray,
colorArraySize,
labels );
classes );
}
break;
case QgsRaster::MultiBandSingleBandGray:

View File

@ -62,20 +62,20 @@ QgsPalettedRendererWidget::QgsPalettedRendererWidget( QgsRasterLayer *layer, con
QgsRasterRenderer *QgsPalettedRendererWidget::renderer()
{
int nColors = mTreeWidget->topLevelItemCount();
QColor *colorArray = new QColor[nColors];
QVector<QString> labels;
QgsPalettedRasterRenderer::ClassData classes;
bool ok = false;
for ( int i = 0; i < nColors; ++i )
{
colorArray[i] = mTreeWidget->topLevelItem( i )->background( 1 ).color();
int value = mTreeWidget->topLevelItem( i )->text( 0 ).toInt( &ok );
if ( !ok )
continue;
QColor color = mTreeWidget->topLevelItem( i )->background( 1 ).color();
QString label = mTreeWidget->topLevelItem( i )->text( 2 );
if ( !label.isEmpty() )
{
if ( i >= labels.size() ) labels.resize( i + 1 );
labels[i] = label;
}
classes.insert( value, QgsPalettedRasterRenderer::Class( color, label ) );
}
int bandNumber = mBandComboBox->currentData().toInt();
return new QgsPalettedRasterRenderer( mRasterLayer->dataProvider(), bandNumber, colorArray, nColors, labels );
return new QgsPalettedRasterRenderer( mRasterLayer->dataProvider(), bandNumber, classes );
}
void QgsPalettedRendererWidget::on_mTreeWidget_itemDoubleClicked( QTreeWidgetItem *item, int column )
@ -110,16 +110,15 @@ void QgsPalettedRendererWidget::setFromRenderer( const QgsRasterRenderer *r )
if ( pr )
{
//read values and colors and fill into tree widget
int nColors = pr->nColors();
QColor *colors = pr->colors();
for ( int i = 0; i < nColors; ++i )
QgsPalettedRasterRenderer::ClassData classes = pr->classes();
QgsPalettedRasterRenderer::ClassData::const_iterator it = classes.constBegin();
for ( ; it != classes.constEnd(); ++it )
{
QTreeWidgetItem *item = new QTreeWidgetItem( mTreeWidget );
item->setText( 0, QString::number( i ) );
item->setBackground( 1, QBrush( colors[i] ) );
item->setText( 2, pr->label( i ) );
item->setText( 0, QString::number( it.key() ) );
item->setBackground( 1, QBrush( it->color ) );
item->setText( 2, it->label );
}
delete[] colors;
}
else
{

View File

@ -32,6 +32,7 @@ from qgis.core import (QgsRaster,
QgsRasterShader,
QgsRasterTransparency,
QgsRenderChecker,
QgsPalettedRasterRenderer,
QgsSingleBandGrayRenderer,
QgsSingleBandPseudoColorRenderer)
from utilities import unitTestDataPath
@ -287,6 +288,81 @@ class TestQgsRasterLayer(unittest.TestCase):
mmoUnserialized.readXml(parentElem)
self.assertEqual(mmo, mmoUnserialized)
def testPaletted(self):
""" test paletted raster renderer with raster with color table"""
path = os.path.join(unitTestDataPath('raster'),
'with_color_table.tif')
info = QFileInfo(path)
base_name = info.baseName()
layer = QgsRasterLayer(path, base_name)
self.assertTrue(layer.isValid(), 'Raster not loaded: {}'.format(path))
renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1,
{3: QgsPalettedRasterRenderer.Class(QColor(255, 0, 0), 'class 1'),
1: QgsPalettedRasterRenderer.Class(QColor(0, 255, 0), 'class 2')})
self.assertEqual(renderer.nColors(), 2)
self.assertEqual(renderer.usesBands(), [1])
# test labels
self.assertEqual(renderer.label(1), 'class 2')
self.assertEqual(renderer.label(3), 'class 1')
self.assertFalse(renderer.label(101))
# test legend symbology - should be sorted by value
legend = renderer.legendSymbologyItems()
self.assertEqual(legend[0][0], 'class 2')
self.assertEqual(legend[1][0], 'class 1')
self.assertEqual(legend[0][1].name(), '#00ff00')
self.assertEqual(legend[1][1].name(), '#ff0000')
# test retrieving classes
classes = renderer.classes()
self.assertEqual(classes[1].label, 'class 2')
self.assertEqual(classes[3].label, 'class 1')
self.assertEqual(classes[1].color.name(), '#00ff00')
self.assertEqual(classes[3].color.name(), '#ff0000')
# test set label
# bad index
renderer.setLabel(1212, 'bad')
renderer.setLabel(3, 'new class')
self.assertEqual(renderer.label(3), 'new class')
# clone
new_renderer = renderer.clone()
classes = new_renderer.classes()
self.assertEqual(classes[1].label, 'class 2')
self.assertEqual(classes[3].label, 'new class')
self.assertEqual(classes[1].color.name(), '#00ff00')
self.assertEqual(classes[3].color.name(), '#ff0000')
# write to xml and read
doc = QDomDocument('testdoc')
elem = doc.createElement('qgis')
renderer.writeXml(doc, elem)
restored = QgsPalettedRasterRenderer.create(elem.firstChild().toElement(), layer.dataProvider())
self.assertTrue(restored)
self.assertEqual(restored.usesBands(), [1])
classes = restored.classes()
self.assertTrue(classes)
self.assertEqual(classes[1].label, 'class 2')
self.assertEqual(classes[3].label, 'new class')
self.assertEqual(classes[1].color.name(), '#00ff00')
self.assertEqual(classes[3].color.name(), '#ff0000')
# render test
layer.setRenderer(renderer)
ms = QgsMapSettings()
ms.setLayers([layer])
ms.setExtent(layer.extent())
checker = QgsRenderChecker()
checker.setControlName("expected_paletted_renderer")
checker.setMapSettings(ms)
self.assertTrue(checker.runTest("expected_paletted_renderer"), "Paletted rendering test failed")
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.