QGIS API Documentation 3.43.0-Master (5250e42f050)
qgsalgorithmfillsinkswangliu.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmfillsinkswangliu.cpp
3 ---------------------
4 begin : April 2025
5 copyright : (C) 2025 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
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
19#include "qgsrasterfilewriter.h"
20#include <queue>
21
23
24QString QgsFillSinksWangLiuAlgorithm::name() const
25{
26 return QStringLiteral( "fillsinkswangliu" );
27}
28
29QString QgsFillSinksWangLiuAlgorithm::displayName() const
30{
31 return QObject::tr( "Fill sinks (Wang & Liu)" );
32}
33
34QStringList QgsFillSinksWangLiuAlgorithm::tags() const
35{
36 return QObject::tr( "fill,filter,slope,dsm,dtm,terrain,water,shed,basin,direction,flow" ).split( ',' );
37}
38
39QString QgsFillSinksWangLiuAlgorithm::group() const
40{
41 return QObject::tr( "Raster terrain analysis" );
42}
43
44QString QgsFillSinksWangLiuAlgorithm::groupId() const
45{
46 return QStringLiteral( "rasterterrainanalysis" );
47}
48
49QString QgsFillSinksWangLiuAlgorithm::shortHelpString() const
50{
51 return QObject::tr( "This algorithm uses a method proposed by Wang & Liu to identify and fill surface depressions in digital elevation models.\n\n"
52
53 "The method was enhanced to allow the creation of hydrologically sound elevation models, i.e. not only to fill the depression(s) "
54 "but also to preserve a downward slope along the flow path. If desired, this is accomplished by preserving a minimum slope "
55 "gradient (and thus elevation difference) between cells.\n\n"
56
57 "References: Wang, L. & H. Liu (2006): An efficient method for identifying and filling surface depressions in digital elevation models for hydrologic analysis and modelling. International Journal of Geographical Information Science, Vol. 20, No. 2: 193-213.\n\n"
58
59 "This algorithm is a port of the SAGA 'Fill Sinks (Wang & Liu)' tool." );
60}
61
62QString QgsFillSinksWangLiuAlgorithm::shortDescription() const
63{
64 return QObject::tr( "Identifies and fills surface depressions in digital elevation models using a method proposed by Wang & Liu." );
65}
66
67void QgsFillSinksWangLiuAlgorithm::initAlgorithm( const QVariantMap & )
68{
69 addParameter( new QgsProcessingParameterRasterLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );
70
71 addParameter( new QgsProcessingParameterBand( QStringLiteral( "BAND" ), QObject::tr( "Band number" ), 1, QStringLiteral( "INPUT" ) ) );
72
73 auto minSlopeParam = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "MIN_SLOPE" ), QObject::tr( "Minimum slope (degrees)" ), Qgis::ProcessingNumberParameterType::Double, 0.1, false, 0 );
74 minSlopeParam->setHelp( QObject::tr( "Minimum slope gradient to preserve from cell to cell. With a value of zero sinks are filled up to the spill elevation (which results in flat areas). Units are degrees." ) );
75 addParameter( minSlopeParam.release() );
76
77 auto createOptsParam = std::make_unique<QgsProcessingParameterString>( QStringLiteral( "CREATION_OPTIONS" ), QObject::tr( "Creation options" ), QVariant(), false, true );
78 createOptsParam->setMetadata( QVariantMap( { { QStringLiteral( "widget_wrapper" ), QVariantMap( { { QStringLiteral( "widget_type" ), QStringLiteral( "rasteroptions" ) } } ) } } ) );
79 createOptsParam->setFlags( createOptsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
80 addParameter( createOptsParam.release() );
81
82 auto outputFilledDem = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_FILLED_DEM" ), QObject::tr( "Output layer (filled DEM)" ), QVariant(), true, true );
83 outputFilledDem->setHelp( QObject::tr( "Depression-free digital elevation model." ) );
84 addParameter( outputFilledDem.release() );
85
86 auto outputFlowDirections = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_FLOW_DIRECTIONS" ), QObject::tr( "Output layer (flow directions)" ), QVariant(), true, false );
87 outputFlowDirections->setHelp( QObject::tr( "Computed flow directions, 0=N, 1=NE, 2=E, ... 7=NW." ) );
88 addParameter( outputFlowDirections.release() );
89
90 auto outputWatershedBasins = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_WATERSHED_BASINS" ), QObject::tr( "Output layer (watershed basins)" ), QVariant(), true, false );
91 outputWatershedBasins->setHelp( QObject::tr( "Delineated watershed basin." ) );
92 addParameter( outputWatershedBasins.release() );
93}
94
95QgsFillSinksWangLiuAlgorithm *QgsFillSinksWangLiuAlgorithm::createInstance() const
96{
97 return new QgsFillSinksWangLiuAlgorithm();
98}
99
100bool QgsFillSinksWangLiuAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
101{
102 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, QStringLiteral( "INPUT" ), context );
103 if ( !layer )
104 throw QgsProcessingException( invalidRasterError( parameters, QStringLiteral( "INPUT" ) ) );
105
106 const int band = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
107
108 mBand = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
109 if ( mBand < 1 || mBand > layer->bandCount() )
110 throw QgsProcessingException( QObject::tr( "Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->bandCount() ) );
111
112 mInterface.reset( layer->dataProvider()->clone() );
113 mHasNoDataValue = layer->dataProvider()->sourceHasNoDataValue( band );
114 mLayerWidth = layer->width();
115 mLayerHeight = layer->height();
116 mExtent = layer->extent();
117 mCrs = layer->crs();
118 mRasterUnitsPerPixelX = layer->rasterUnitsPerPixelX();
119 mRasterUnitsPerPixelY = layer->rasterUnitsPerPixelY();
120 mRasterDiagonal = std::sqrt( mRasterUnitsPerPixelX * mRasterUnitsPerPixelX + mRasterUnitsPerPixelY * mRasterUnitsPerPixelY );
121 mDataType = layer->dataProvider()->dataType( mBand );
122 mNoData = layer->dataProvider()->sourceNoDataValue( mBand );
123 mDirectionalLengths = { mRasterUnitsPerPixelY, mRasterDiagonal, mRasterUnitsPerPixelX, mRasterDiagonal, mRasterUnitsPerPixelY, mRasterDiagonal, mRasterUnitsPerPixelX, mRasterDiagonal };
124 return true;
125}
126
127static constexpr std::array< int, 8 > COL_DIRECTION_OFFSETS { 0, 1, 1, 1, 0, -1, -1, -1 };
128static constexpr std::array< int, 8 > ROW_DIRECTION_OFFSETS { -1, -1, 0, 1, 1, 1, 0, -1 };
129
130bool QgsFillSinksWangLiuAlgorithm::isInGrid( int row, int col ) const
131{
132 return col >= 0 && col < mLayerWidth && row >= 0 && row < mLayerHeight;
133}
134
135QgsFillSinksWangLiuAlgorithm::Direction QgsFillSinksWangLiuAlgorithm::getDir( int row, int col, double z, const QgsRasterBlock *filled ) const
136{
137 Direction steepestDirection = Invalid;
138 double maxGradient = 0;
139 bool isNoData = false;
140
141 for ( Direction direction : { North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest } )
142 {
143 const int neighborCol = col + COL_DIRECTION_OFFSETS[direction];
144 const int neighborRow = row + ROW_DIRECTION_OFFSETS[direction];
145
146 if ( isInGrid( neighborRow, neighborCol ) )
147 {
148 const double neighborZ = filled->valueAndNoData( neighborRow, neighborCol, isNoData );
149 if ( !isNoData && neighborZ < z )
150 {
151 const double gradient = ( z - neighborZ ) / mDirectionalLengths[direction];
152 if ( gradient >= maxGradient )
153 {
154 maxGradient = gradient;
155 steepestDirection = direction;
156 }
157 }
158 }
159 }
160
161 return steepestDirection;
162}
163
164struct CFillSinks_WL_Node
165{
166 int row = 0;
167 int col = 0;
168 double spill = 0;
169};
170
171class CompareGreater
172{
173 public:
174 bool operator()( CFillSinks_WL_Node n1, CFillSinks_WL_Node n2 ) const
175 {
176 return n1.spill > n2.spill;
177 }
178};
179
180typedef std::vector< CFillSinks_WL_Node > nodeVector;
181typedef std::priority_queue< CFillSinks_WL_Node, nodeVector, CompareGreater > PriorityQ;
182
183QVariantMap QgsFillSinksWangLiuAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
184{
185 const QString createOptions = parameterAsString( parameters, QStringLiteral( "CREATION_OPTIONS" ), context ).trimmed();
186
187 const QString filledDemOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_FILLED_DEM" ), context );
188 const QString flowDirectionsOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_FLOW_DIRECTIONS" ), context );
189 const QString watershedBasinsOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_WATERSHED_BASINS" ), context );
190
191 std::unique_ptr<QgsRasterFileWriter> filledDemWriter;
192 std::unique_ptr<QgsRasterDataProvider> filledDemDestProvider;
193
194 if ( !filledDemOutputFile.isEmpty() )
195 {
196 const QFileInfo fi( filledDemOutputFile );
197 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
198
199 filledDemWriter = std::make_unique<QgsRasterFileWriter>( filledDemOutputFile );
200 filledDemWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
201 if ( !createOptions.isEmpty() )
202 {
203 filledDemWriter->setCreationOptions( createOptions.split( '|' ) );
204 }
205 filledDemWriter->setOutputFormat( outputFormat );
206
207 filledDemDestProvider.reset( filledDemWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
208
209 if ( !filledDemDestProvider )
210 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( filledDemOutputFile ) );
211 if ( !filledDemDestProvider->isValid() )
212 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( filledDemOutputFile, filledDemDestProvider->error().message( QgsErrorMessage::Text ) ) );
213
214 filledDemDestProvider->setNoDataValue( 1, mNoData );
215 filledDemDestProvider->setEditable( true );
216 }
217
218 std::unique_ptr<QgsRasterFileWriter> flowDirectionsWriter;
219 std::unique_ptr<QgsRasterDataProvider> flowDirectionsDestProvider;
220
221 if ( !flowDirectionsOutputFile.isEmpty() )
222 {
223 const QFileInfo fi( flowDirectionsOutputFile );
224 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
225
226 flowDirectionsWriter = std::make_unique<QgsRasterFileWriter>( flowDirectionsOutputFile );
227 flowDirectionsWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
228 flowDirectionsWriter->setOutputFormat( outputFormat );
229
230 flowDirectionsDestProvider.reset( flowDirectionsWriter->createOneBandRaster( Qgis::DataType::Byte, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
231
232 if ( !flowDirectionsDestProvider )
233 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( flowDirectionsOutputFile ) );
234 if ( !flowDirectionsDestProvider->isValid() )
235 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( flowDirectionsOutputFile, flowDirectionsDestProvider->error().message( QgsErrorMessage::Text ) ) );
236
237 flowDirectionsDestProvider->setNoDataValue( 1, 255 );
238 flowDirectionsDestProvider->setEditable( true );
239 }
240
241 std::unique_ptr<QgsRasterFileWriter> watershedBasinsWriter;
242 std::unique_ptr<QgsRasterDataProvider> watershedBasinsDestProvider;
243
244 if ( !watershedBasinsOutputFile.isEmpty() )
245 {
246 const QFileInfo fi( watershedBasinsOutputFile );
247 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
248
249 watershedBasinsWriter = std::make_unique<QgsRasterFileWriter>( watershedBasinsOutputFile );
250 watershedBasinsWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
251 watershedBasinsWriter->setOutputFormat( outputFormat );
252
253 watershedBasinsDestProvider.reset( watershedBasinsWriter->createOneBandRaster( Qgis::DataType::Int32, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
254
255 if ( !watershedBasinsDestProvider )
256 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( watershedBasinsOutputFile ) );
257 if ( !watershedBasinsDestProvider->isValid() )
258 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( watershedBasinsOutputFile, watershedBasinsDestProvider->error().message( QgsErrorMessage::Text ) ) );
259
260 watershedBasinsDestProvider->setNoDataValue( 1, -1 );
261 watershedBasinsDestProvider->setEditable( true );
262 }
263
264 std::unique_ptr< QgsRasterBlock > sourceDemData( mInterface->block( mBand, mExtent, mLayerWidth, mLayerHeight ) );
265 if ( !sourceDemData )
266 {
267 throw QgsProcessingException( QObject::tr( "Could not read DEM raster" ) );
268 }
269
270 auto filledDemData = std::make_unique<QgsRasterBlock>( mDataType, mLayerWidth, mLayerHeight );
271 filledDemData->setNoDataValue( mNoData );
272 filledDemData->setIsNoData();
273
274 auto watershedData = std::make_unique<QgsRasterBlock>( Qgis::DataType::Int32, mLayerWidth, mLayerHeight );
275 watershedData->setNoDataValue( -1 );
276 watershedData->setIsNoData();
277
278 auto outputFlowData = std::make_unique<QgsRasterBlock>( Qgis::DataType::Byte, mLayerWidth, mLayerHeight );
279 outputFlowData->setNoDataValue( 255 );
280 outputFlowData->setIsNoData();
281
282 auto seedData = std::make_unique<QgsRasterBlock>( Qgis::DataType::Byte, mLayerWidth, mLayerHeight );
283 seedData->fill( 0 );
284
285 double minSlope = parameterAsDouble( parameters, QStringLiteral( "MIN_SLOPE" ), context );
286 double mindiff[8];
287 bool preserve = false;
288 if ( minSlope > 0.0 )
289 {
290 minSlope = tan( minSlope * M_PI / 180.0 );
291 for ( int i = 0; i < 8; i++ )
292 mindiff[i] = minSlope * mDirectionalLengths[i];
293 preserve = true;
294 }
295
296 // fill priority queue with boundary, i.e. seed cells
297 CFillSinks_WL_Node tempNode;
298 PriorityQ theQueue;
299
300 long long id = 0;
301 double value = 0;
302 bool isNoData = false;
303 feedback->setProgressText( QObject::tr( "Seed boundary cells" ) );
304
305 std::size_t processed = 0;
306 const std::size_t totalCells = static_cast< std::size_t >( mLayerWidth ) * mLayerHeight;
307
308 for ( int row = 0; row < mLayerHeight; row++ )
309 {
310 if ( feedback->isCanceled() )
311 break;
312
313 for ( int col = 0; col < mLayerWidth; col++ )
314 {
315 value = sourceDemData->valueAndNoData( row, col, isNoData );
316 if ( !isNoData )
317 {
318 for ( Direction direction : { North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest } )
319 {
320 int iCol = col + COL_DIRECTION_OFFSETS[direction];
321 int iRow = row + ROW_DIRECTION_OFFSETS[direction];
322 ;
323 if ( !isInGrid( iRow, iCol ) || sourceDemData->isNoData( iRow, iCol ) )
324 {
325 const double z = value;
326 filledDemData->setValue( row, col, z );
327 seedData->setValue( row, col, 1.0 );
328 watershedData->setValue( row, col, static_cast< double >( id ) );
329 id += 1;
330
331 tempNode.row = row;
332 tempNode.col = col;
333 tempNode.spill = z;
334 theQueue.push( tempNode );
335 processed += 1;
336 break;
337 }
338 }
339 }
340 feedback->setProgress( static_cast< double >( processed ) / static_cast< double >( totalCells ) * 100 );
341 }
342 }
343
344 if ( feedback->isCanceled() )
345 return {};
346
347 // work through least cost path
348 feedback->setProgressText( QObject::tr( "Filling using least cost paths" ) );
349
350 while ( !theQueue.empty() )
351 {
352 PriorityQ::value_type tempNode = theQueue.top();
353
354 const int row = tempNode.row;
355 const int col = tempNode.col;
356 const double z = tempNode.spill;
357 theQueue.pop();
358
359 const long long id = static_cast< long long >( watershedData->value( row, col ) );
360
361 for ( Direction direction : { North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest } )
362 {
363 const int iCol = col + COL_DIRECTION_OFFSETS[direction];
364 const int iRow = row + ROW_DIRECTION_OFFSETS[direction];
365 isNoData = false;
366 const bool iInGrid = isInGrid( iRow, iCol );
367 double iz = iInGrid ? sourceDemData->valueAndNoData( iRow, iCol, isNoData ) : 0;
368 if ( iInGrid && !isNoData )
369 {
370 if ( filledDemData->isNoData( iRow, iCol ) )
371 {
372 if ( preserve )
373 {
374 iz = std::max( iz, z + mindiff[static_cast< int >( direction )] );
375 }
376 else if ( iz <= z )
377 {
378 iz = z;
379 outputFlowData->setValue( iRow, iCol, INVERSE_DIRECTION[static_cast< int >( direction )] );
380 }
381
382 tempNode.row = iRow;
383 tempNode.col = iCol;
384 tempNode.spill = iz;
385 theQueue.push( tempNode );
386
387 filledDemData->setValue( iRow, iCol, iz );
388 watershedData->setValue( iRow, iCol, id );
389 processed += 1;
390 }
391 else if ( seedData->value( iRow, iCol ) == 1 )
392 {
393 watershedData->setValue( iRow, iCol, id );
394 }
395 }
396 }
397
398 if ( outputFlowData->isNoData( row, col ) )
399 outputFlowData->setValue( row, col, getDir( row, col, z, filledDemData.get() ) );
400
401 feedback->setProgress( static_cast< double >( processed ) / static_cast< double >( totalCells ) * 100 );
402 if ( feedback->isCanceled() )
403 break;
404 }
405
406 if ( feedback->isCanceled() )
407 return {};
408
409 QVariantMap outputs;
410
411 if ( filledDemDestProvider )
412 {
413 if ( !filledDemDestProvider->writeBlock( filledDemData.get(), 1, 0, 0 ) )
414 {
415 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( filledDemDestProvider->error().summary() ) );
416 }
417 filledDemDestProvider->setEditable( false );
418 outputs.insert( QStringLiteral( "OUTPUT_FILLED_DEM" ), filledDemOutputFile );
419 }
420 if ( flowDirectionsDestProvider )
421 {
422 if ( !flowDirectionsDestProvider->writeBlock( outputFlowData.get(), 1, 0, 0 ) )
423 {
424 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( flowDirectionsDestProvider->error().summary() ) );
425 }
426 flowDirectionsDestProvider->setEditable( false );
427 outputs.insert( QStringLiteral( "OUTPUT_FLOW_DIRECTIONS" ), flowDirectionsOutputFile );
428 }
429 if ( watershedBasinsDestProvider )
430 {
431 if ( !watershedBasinsDestProvider->writeBlock( watershedData.get(), 1, 0, 0 ) )
432 {
433 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( watershedBasinsDestProvider->error().summary() ) );
434 }
435 watershedBasinsDestProvider->setEditable( false );
436 outputs.insert( QStringLiteral( "OUTPUT_WATERSHED_BASINS" ), watershedBasinsOutputFile );
437 }
438
439 return outputs;
440}
441
442
@ Byte
Eight bit unsigned integer (quint8)
@ Int32
Thirty two bit signed integer (qint32)
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
virtual QgsRectangle extent() const
Returns the extent of the layer.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:84
Contains information about the context in which a processing algorithm is executed.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
virtual void setProgressText(const QString &text)
Sets a progress report text string.
A raster band parameter for Processing algorithms.
A raster layer parameter for processing algorithms.
Raster data container.
double valueAndNoData(int row, int column, bool &isNoData) const
Reads a single value from the pixel at row and column, if type of block is numeric.
QgsRasterDataProvider * clone() const override=0
Clone itself, create deep copy.
virtual bool sourceHasNoDataValue(int bandNo) const
Returns true if source band has no data value.
virtual double sourceNoDataValue(int bandNo) const
Value representing no data value.
Qgis::DataType dataType(int bandNo) const override=0
Returns data type for the band specified by number.
static QString driverForExtension(const QString &extension)
Returns the GDAL driver name for a specified file extension.
Represents a raster layer.
int height() const
Returns the height of the (unclipped) raster.
int bandCount() const
Returns the number of bands in this layer.
double rasterUnitsPerPixelX() const
Returns the number of raster units per each raster pixel in X axis.
QgsRasterDataProvider * dataProvider() override
Returns the source data provider.
double rasterUnitsPerPixelY() const
Returns the number of raster units per each raster pixel in Y axis.
int width() const
Returns the width of the (unclipped) raster.