1 /***************************************************************************
2 qgssinglebandgrayrenderer.cpp
3 -----------------------------
4 begin : December 2011
5 copyright : (C) 2011 by Marco Hugentobler
6 email : marco at sourcepole dot ch
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include "qgssinglebandgrayrenderer.h"
19 #include "qgscontrastenhancement.h"
20 #include "qgsrastertransparency.h"
21 #include "qgscolorramplegendnode.h"
22 #include "qgscolorramplegendnodesettings.h"
23 #include "qgsreadwritecontext.h"
24 #include "qgscolorramp.h"
25 #include "qgssymbol.h"
26
27 #include <QDomDocument>
28 #include <QDomElement>
29 #include <QImage>
30 #include <QColor>
31 #include <memory>
32
QgsSingleBandGrayRenderer(QgsRasterInterface * input,int grayBand)33 QgsSingleBandGrayRenderer::QgsSingleBandGrayRenderer( QgsRasterInterface *input, int grayBand )
34 : QgsRasterRenderer( input, QStringLiteral( "singlebandgray" ) )
35 , mGrayBand( grayBand )
36 , mGradient( BlackToWhite )
37 , mContrastEnhancement( nullptr )
38 , mLegendSettings( std::make_unique< QgsColorRampLegendNodeSettings >() )
39 {
40 }
41
clone() const42 QgsSingleBandGrayRenderer *QgsSingleBandGrayRenderer::clone() const
43 {
44 QgsSingleBandGrayRenderer *renderer = new QgsSingleBandGrayRenderer( nullptr, mGrayBand );
45 renderer->copyCommonProperties( this );
46
47 renderer->setGradient( mGradient );
48 if ( mContrastEnhancement )
49 {
50 renderer->setContrastEnhancement( new QgsContrastEnhancement( *mContrastEnhancement ) );
51 }
52 renderer->setLegendSettings( mLegendSettings ? new QgsColorRampLegendNodeSettings( *mLegendSettings.get() ) : new QgsColorRampLegendNodeSettings() );
53 return renderer;
54 }
55
create(const QDomElement & elem,QgsRasterInterface * input)56 QgsRasterRenderer *QgsSingleBandGrayRenderer::create( const QDomElement &elem, QgsRasterInterface *input )
57 {
58 if ( elem.isNull() )
59 {
60 return nullptr;
61 }
62
63 const int grayBand = elem.attribute( QStringLiteral( "grayBand" ), QStringLiteral( "-1" ) ).toInt();
64 QgsSingleBandGrayRenderer *r = new QgsSingleBandGrayRenderer( input, grayBand );
65 r->readXml( elem );
66
67 if ( elem.attribute( QStringLiteral( "gradient" ) ) == QLatin1String( "WhiteToBlack" ) )
68 {
69 r->setGradient( WhiteToBlack ); // BlackToWhite is default
70 }
71
72 const QDomElement contrastEnhancementElem = elem.firstChildElement( QStringLiteral( "contrastEnhancement" ) );
73 if ( !contrastEnhancementElem.isNull() )
74 {
75 QgsContrastEnhancement *ce = new QgsContrastEnhancement( ( Qgis::DataType )(
76 input->dataType( grayBand ) ) );
77 ce->readXml( contrastEnhancementElem );
78 r->setContrastEnhancement( ce );
79 }
80
81 std::unique_ptr< QgsColorRampLegendNodeSettings > legendSettings = std::make_unique< QgsColorRampLegendNodeSettings >();
82 legendSettings->readXml( elem, QgsReadWriteContext() );
83 r->setLegendSettings( legendSettings.release() );
84
85 return r;
86 }
87
setContrastEnhancement(QgsContrastEnhancement * ce)88 void QgsSingleBandGrayRenderer::setContrastEnhancement( QgsContrastEnhancement *ce )
89 {
90 mContrastEnhancement.reset( ce );
91 }
92
block(int bandNo,const QgsRectangle & extent,int width,int height,QgsRasterBlockFeedback * feedback)93 QgsRasterBlock *QgsSingleBandGrayRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback )
94 {
95 Q_UNUSED( bandNo )
96 QgsDebugMsgLevel( QStringLiteral( "width = %1 height = %2" ).arg( width ).arg( height ), 4 );
97
98 std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() );
99 if ( !mInput )
100 {
101 return outputBlock.release();
102 }
103
104 const std::shared_ptr< QgsRasterBlock > inputBlock( mInput->block( mGrayBand, extent, width, height, feedback ) );
105 if ( !inputBlock || inputBlock->isEmpty() )
106 {
107 QgsDebugMsg( QStringLiteral( "No raster data!" ) );
108 return outputBlock.release();
109 }
110
111 std::shared_ptr< QgsRasterBlock > alphaBlock;
112
113 if ( mAlphaBand > 0 && mGrayBand != mAlphaBand )
114 {
115 alphaBlock.reset( mInput->block( mAlphaBand, extent, width, height, feedback ) );
116 if ( !alphaBlock || alphaBlock->isEmpty() )
117 {
118 // TODO: better to render without alpha
119 return outputBlock.release();
120 }
121 }
122 else if ( mAlphaBand > 0 )
123 {
124 alphaBlock = inputBlock;
125 }
126
127 if ( !outputBlock->reset( Qgis::DataType::ARGB32_Premultiplied, width, height ) )
128 {
129 return outputBlock.release();
130 }
131
132 const QRgb myDefaultColor = renderColorForNodataPixel();
133 bool isNoData = false;
134 for ( qgssize i = 0; i < ( qgssize )width * height; i++ )
135 {
136 double grayVal = inputBlock->valueAndNoData( i, isNoData );
137
138 if ( isNoData )
139 {
140 outputBlock->setColor( i, myDefaultColor );
141 continue;
142 }
143
144 double currentAlpha = mOpacity;
145 if ( mRasterTransparency )
146 {
147 currentAlpha = mRasterTransparency->alphaValue( grayVal, mOpacity * 255 ) / 255.0;
148 }
149 if ( mAlphaBand > 0 )
150 {
151 currentAlpha *= alphaBlock->value( i ) / 255.0;
152 }
153
154 if ( mContrastEnhancement )
155 {
156 if ( !mContrastEnhancement->isValueInDisplayableRange( grayVal ) )
157 {
158 outputBlock->setColor( i, myDefaultColor );
159 continue;
160 }
161 grayVal = mContrastEnhancement->enhanceContrast( grayVal );
162 }
163
164 if ( mGradient == WhiteToBlack )
165 {
166 grayVal = 255 - grayVal;
167 }
168
169 if ( qgsDoubleNear( currentAlpha, 1.0 ) )
170 {
171 outputBlock->setColor( i, qRgba( grayVal, grayVal, grayVal, 255 ) );
172 }
173 else
174 {
175 outputBlock->setColor( i, qRgba( currentAlpha * grayVal, currentAlpha * grayVal, currentAlpha * grayVal, currentAlpha * 255 ) );
176 }
177 }
178
179 return outputBlock.release();
180 }
181
writeXml(QDomDocument & doc,QDomElement & parentElem) const182 void QgsSingleBandGrayRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const
183 {
184 if ( parentElem.isNull() )
185 {
186 return;
187 }
188
189 QDomElement rasterRendererElem = doc.createElement( QStringLiteral( "rasterrenderer" ) );
190 _writeXml( doc, rasterRendererElem );
191
192 rasterRendererElem.setAttribute( QStringLiteral( "grayBand" ), mGrayBand );
193
194 QString gradient;
195 if ( mGradient == BlackToWhite )
196 {
197 gradient = QStringLiteral( "BlackToWhite" );
198 }
199 else
200 {
201 gradient = QStringLiteral( "WhiteToBlack" );
202 }
203 rasterRendererElem.setAttribute( QStringLiteral( "gradient" ), gradient );
204
205 if ( mContrastEnhancement )
206 {
207 QDomElement contrastElem = doc.createElement( QStringLiteral( "contrastEnhancement" ) );
208 mContrastEnhancement->writeXml( doc, contrastElem );
209 rasterRendererElem.appendChild( contrastElem );
210 }
211
212 if ( mLegendSettings )
213 mLegendSettings->writeXml( doc, rasterRendererElem, QgsReadWriteContext() );
214
215 parentElem.appendChild( rasterRendererElem );
216 }
217
legendSymbologyItems() const218 QList<QPair<QString, QColor> > QgsSingleBandGrayRenderer::legendSymbologyItems() const
219 {
220 QList<QPair<QString, QColor> > symbolItems;
221 if ( mContrastEnhancement && mContrastEnhancement->contrastEnhancementAlgorithm() != QgsContrastEnhancement::NoEnhancement )
222 {
223 const QColor minColor = ( mGradient == BlackToWhite ) ? Qt::black : Qt::white;
224 const QColor maxColor = ( mGradient == BlackToWhite ) ? Qt::white : Qt::black;
225 symbolItems.push_back( qMakePair( QString::number( mContrastEnhancement->minimumValue() ), minColor ) );
226 symbolItems.push_back( qMakePair( QString::number( mContrastEnhancement->maximumValue() ), maxColor ) );
227 }
228 return symbolItems;
229 }
230
createLegendNodes(QgsLayerTreeLayer * nodeLayer)231 QList<QgsLayerTreeModelLegendNode *> QgsSingleBandGrayRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer )
232 {
233 QList<QgsLayerTreeModelLegendNode *> res;
234 if ( mContrastEnhancement && mContrastEnhancement->contrastEnhancementAlgorithm() != QgsContrastEnhancement::NoEnhancement )
235 {
236 const QString name = displayBandName( mGrayBand );
237 if ( !name.isEmpty() )
238 {
239 res << new QgsSimpleLegendNode( nodeLayer, name );
240 }
241
242 const QColor minColor = ( mGradient == BlackToWhite ) ? Qt::black : Qt::white;
243 const QColor maxColor = ( mGradient == BlackToWhite ) ? Qt::white : Qt::black;
244 res << new QgsColorRampLegendNode( nodeLayer, new QgsGradientColorRamp( minColor, maxColor ),
245 mLegendSettings ? *mLegendSettings : QgsColorRampLegendNodeSettings(),
246 mContrastEnhancement->minimumValue(),
247 mContrastEnhancement->maximumValue() );
248 }
249 return res;
250 }
251
usesBands() const252 QList<int> QgsSingleBandGrayRenderer::usesBands() const
253 {
254 QList<int> bandList;
255 if ( mGrayBand != -1 )
256 {
257 bandList << mGrayBand;
258 }
259 return bandList;
260 }
261
toSld(QDomDocument & doc,QDomElement & element,const QVariantMap & props) const262 void QgsSingleBandGrayRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const
263 {
264 // create base structure
265 QgsRasterRenderer::toSld( doc, element, props );
266
267 // look for RasterSymbolizer tag
268 QDomNodeList elements = element.elementsByTagName( QStringLiteral( "sld:RasterSymbolizer" ) );
269 if ( elements.size() == 0 )
270 return;
271
272 // there SHOULD be only one
273 QDomElement rasterSymbolizerElem = elements.at( 0 ).toElement();
274
275 // add Channel Selection tags
276 // Need to insert channelSelection in the correct sequence as in SLD standard e.g.
277 // after opacity or geometry or as first element after sld:RasterSymbolizer
278 QDomElement channelSelectionElem = doc.createElement( QStringLiteral( "sld:ChannelSelection" ) );
279 elements = rasterSymbolizerElem.elementsByTagName( QStringLiteral( "sld:Opacity" ) );
280 if ( elements.size() != 0 )
281 {
282 rasterSymbolizerElem.insertAfter( channelSelectionElem, elements.at( 0 ) );
283 }
284 else
285 {
286 elements = rasterSymbolizerElem.elementsByTagName( QStringLiteral( "sld:Geometry" ) );
287 if ( elements.size() != 0 )
288 {
289 rasterSymbolizerElem.insertAfter( channelSelectionElem, elements.at( 0 ) );
290 }
291 else
292 {
293 rasterSymbolizerElem.insertBefore( channelSelectionElem, rasterSymbolizerElem.firstChild() );
294 }
295 }
296
297 // for gray band
298 QDomElement channelElem = doc.createElement( QStringLiteral( "sld:GrayChannel" ) );
299 channelSelectionElem.appendChild( channelElem );
300
301 // set band
302 QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) );
303 sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( grayBand() ) ) );
304 channelElem.appendChild( sourceChannelNameElem );
305
306 // set ContrastEnhancement
307 if ( auto *lContrastEnhancement = contrastEnhancement() )
308 {
309 QDomElement contrastEnhancementElem = doc.createElement( QStringLiteral( "sld:ContrastEnhancement" ) );
310 lContrastEnhancement->toSld( doc, contrastEnhancementElem );
311
312 // do changes to minValue/maxValues depending on stretching algorithm. This is necessary because
313 // geoserver does a first stretch on min/max, then applies color map rules.
314 // In some combination it is necessary to use real min/max values and in
315 // others the actual edited min/max values
316 switch ( lContrastEnhancement->contrastEnhancementAlgorithm() )
317 {
318 case QgsContrastEnhancement::StretchAndClipToMinimumMaximum:
319 case QgsContrastEnhancement::ClipToMinimumMaximum:
320 {
321 // with this renderer export have to be check against real min/max values of the raster
322 const QgsRasterBandStats myRasterBandStats = mInput->bandStatistics( grayBand(), QgsRasterBandStats::Min | QgsRasterBandStats::Max );
323
324 // if minimum range differ from the real minimum => set is in exported SLD vendor option
325 if ( !qgsDoubleNear( lContrastEnhancement->minimumValue(), myRasterBandStats.minimumValue ) )
326 {
327 // look for VendorOption tag to look for that with minValue attribute
328 const QDomNodeList vendorOptions = contrastEnhancementElem.elementsByTagName( QStringLiteral( "sld:VendorOption" ) );
329 for ( int i = 0; i < vendorOptions.size(); ++i )
330 {
331 QDomElement vendorOption = vendorOptions.at( i ).toElement();
332 if ( vendorOption.attribute( QStringLiteral( "name" ) ) != QLatin1String( "minValue" ) )
333 continue;
334
335 // remove old value and add the new one
336 vendorOption.removeChild( vendorOption.firstChild() );
337 vendorOption.appendChild( doc.createTextNode( QString::number( myRasterBandStats.minimumValue ) ) );
338 }
339 }
340 break;
341 }
342 case QgsContrastEnhancement::UserDefinedEnhancement:
343 break;
344 case QgsContrastEnhancement::NoEnhancement:
345 break;
346 case QgsContrastEnhancement::StretchToMinimumMaximum:
347 break;
348 }
349
350 channelElem.appendChild( contrastEnhancementElem );
351 }
352
353 // for each color set a ColorMapEntry tag nested into "sld:ColorMap" tag
354 // e.g. <ColorMapEntry color="#EEBE2F" quantity="-300" label="label" opacity="0"/>
355 QList< QPair< QString, QColor > > classes = legendSymbologyItems();
356
357 // add ColorMap tag
358 QDomElement colorMapElem = doc.createElement( QStringLiteral( "sld:ColorMap" ) );
359 rasterSymbolizerElem.appendChild( colorMapElem );
360
361 // TODO: add clip intervals basing on real min/max without trigger
362 // min/max calculation again that can takes a lot for remote or big images
363 //
364 // contrast enhancement against a color map can be SLD simulated playing with ColorMapEntryies
365 // each ContrastEnhancementAlgorithm need a specific management.
366 // set type of ColorMap ramp [ramp, intervals, values]
367 // basing on interpolation algorithm of the raster shader
368 QList< QPair< QString, QColor > > colorMapping( classes );
369 switch ( contrastEnhancement()->contrastEnhancementAlgorithm() )
370 {
371 case ( QgsContrastEnhancement::StretchAndClipToMinimumMaximum ):
372 case ( QgsContrastEnhancement::ClipToMinimumMaximum ):
373 {
374 const QString lowValue = classes[0].first;
375 QColor lowColor = classes[0].second;
376 lowColor.setAlpha( 0 );
377 const QString highValue = classes[1].first;
378 QColor highColor = classes[1].second;
379 highColor.setAlpha( 0 );
380
381 colorMapping.prepend( QPair< QString, QColor >( lowValue, lowColor ) );
382 colorMapping.append( QPair< QString, QColor >( highValue, highColor ) );
383 break;
384 }
385 case ( QgsContrastEnhancement::StretchToMinimumMaximum ):
386 {
387 colorMapping[0].first = QStringLiteral( "0" );
388 colorMapping[1].first = QStringLiteral( "255" );
389 break;
390 }
391 case ( QgsContrastEnhancement::UserDefinedEnhancement ):
392 break;
393 case ( QgsContrastEnhancement::NoEnhancement ):
394 break;
395 }
396
397 // create tags
398 for ( auto it = colorMapping.constBegin(); it != colorMapping.constEnd() ; ++it )
399 {
400 // set low level color mapping
401 QDomElement lowColorMapEntryElem = doc.createElement( QStringLiteral( "sld:ColorMapEntry" ) );
402 colorMapElem.appendChild( lowColorMapEntryElem );
403 lowColorMapEntryElem.setAttribute( QStringLiteral( "color" ), it->second.name() );
404 lowColorMapEntryElem.setAttribute( QStringLiteral( "quantity" ), it->first );
405 if ( it->second.alphaF() == 0.0 )
406 {
407 lowColorMapEntryElem.setAttribute( QStringLiteral( "opacity" ), QString::number( it->second.alpha() ) );
408 }
409 }
410 }
411
legendSettings() const412 const QgsColorRampLegendNodeSettings *QgsSingleBandGrayRenderer::legendSettings() const
413 {
414 return mLegendSettings.get();
415 }
416
setLegendSettings(QgsColorRampLegendNodeSettings * settings)417 void QgsSingleBandGrayRenderer::setLegendSettings( QgsColorRampLegendNodeSettings *settings )
418 {
419 if ( settings == mLegendSettings.get() )
420 return;
421 mLegendSettings.reset( settings );
422 }
423