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