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