QGIS API Documentation 4.3.0-Master (45633be667c)
Loading...
Searching...
No Matches
qgsalgorithmchannelnetwork.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmchannelnetwork.cpp
3 ---------------------
4 begin : September 2026
5 copyright : (C) 2026 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
20#include <gdal.h>
21#include <gdal_alg.h>
22#include <ogrsf_frmts.h>
23
24#include "qgslinestring.h"
25#include "qgsogrutils.h"
27
28#include <QString>
29
30using namespace Qt::StringLiterals;
31
33
34QStringList QgsChannelNetworkAlgorithmBase::tags() const
35{
36 return QObject::tr( "dem,channels,network,basins,drainage,junctions,strahler,hydrology" ).split( ',' );
37}
38
39void QgsChannelNetworkAlgorithmBase::addCommonParameters()
40{
41 auto thresholdParam = std::make_unique<QgsProcessingParameterNumber>( u"THRESHOLD"_s, QObject::tr( "Minimum stream order threshold" ), Qgis::ProcessingNumberParameterType::Integer, 5, false, 1 );
42 thresholdParam->setHelp(
43 QObject::tr(
44 "Minimum Strahler stream order required to initiate a channel segment. Cells with a Strahler order equal to or greater than this threshold will be extracted as channel networks. Output stream "
45 "orders on extracted vector features will be shifted so that the threshold order equals order 1."
46 )
47 );
48 addParameter( thresholdParam.release() );
49
50 auto subbasinsParam = std::make_unique<QgsProcessingParameterBoolean>( u"SUBBASINS"_s, QObject::tr( "Delineate subbasins" ), true );
51 subbasinsParam->setHelp(
52 QObject::tr( "If checked, individual subbasins will be delineated for every channel junction and tributary confluence. If unchecked, only major drainage basins for outer outlet nodes will be generated." )
53 );
54 addParameter( subbasinsParam.release() );
55
56 addParameter( new QgsProcessingParameterVectorDestination( u"CHANNELS"_s, QObject::tr( "Channels" ), Qgis::ProcessingSourceType::VectorLine, QVariant(), true, true ) );
57 addParameter( new QgsProcessingParameterVectorDestination( u"BASINS"_s, QObject::tr( "Drainage basins" ), Qgis::ProcessingSourceType::VectorPolygon, QVariant(), true, true ) );
58 addParameter( new QgsProcessingParameterVectorDestination( u"JUNCTIONS"_s, QObject::tr( "Junctions" ), Qgis::ProcessingSourceType::VectorPoint, QVariant(), true, true ) );
59}
60
61void QgsChannelNetworkAlgorithmBase::extractChannelNetwork(
62 const QgsRasterBlock *demBlock,
63 const std::vector<int8_t> &d8Directions,
64 const std::vector<int16_t> &strahlerOrders,
65 int width,
66 int height,
67 const QgsRectangle &extent,
69 int threshold,
70 bool subbasins,
71 QgsFeatureSink *channelSink,
72 QgsFeatureSink *basinSink,
73 QgsFeatureSink *junctionSink,
74 QgsProcessingFeedback *feedback,
75 const QVariantMap &parameters
76)
77{
78 const std::size_t totalCells = static_cast<std::size_t>( width ) * height;
79 const double cellWidth = extent.width() / width;
80 const double cellHeight = extent.height() / height;
81
82 std::vector<int32_t> nodesGrid( totalCells, 0 );
83 std::vector<int32_t> basinsGrid( totalCells, 0 );
84
85 QgsProcessingMultiStepFeedback multiStepFeedback( 5, feedback );
86 multiStepFeedback.setStepWeights( { 1, 1, 1, 0.5, 4 } );
87
88 for ( int row = 0; row < height; ++row )
89 {
90 for ( int col = 0; col < width; ++col )
91 {
92 const qgssize idx = static_cast<qgssize>( row ) * width + col;
93 basinsGrid[idx] = demBlock->isNoData( row, col ) ? 0 : -1;
94 }
95 }
96
97 int nNodes = 0;
98 int nBasins = 0;
99 QHash<int, int> basinToOrderMap;
100
101 auto setNode = [&]( int column, int row, int id, NodeType type, int rawOrder, int basinId ) {
102 if ( type != NodeType::Mouth )
103 {
104 nodesGrid[static_cast<qgssize>( row ) * width + column] = id;
105 }
106
107 const int shiftedOrder = rawOrder + 1 - threshold;
108 if ( type == NodeType::Outlet || type == NodeType::Mouth )
109 {
110 basinToOrderMap[basinId] = shiftedOrder;
111 }
112
113 if ( junctionSink )
114 {
115 double xWorld;
116 double yWorld;
117 QgsRasterAnalysisUtils::pixelToMap( column, row, extent, cellWidth, cellHeight, xWorld, yWorld );
118 const double z = demBlock->value( row, column );
119
120 QString typeStr;
121 switch ( type )
122 {
123 case NodeType::Spring:
124 typeStr = u"Spring"_s;
125 break;
126 case NodeType::Junction:
127 typeStr = u"Junction"_s;
128 break;
129 case NodeType::Outlet:
130 typeStr = u"Outlet"_s;
131 break;
132 case NodeType::Mouth:
133 typeStr = u"Mouth"_s;
134 break;
135 }
136
137 QgsFeature feat;
138 feat.setGeometry( QgsGeometry( std::make_unique<QgsPoint>( xWorld, yWorld, z ) ) );
139 feat.setAttributes( QgsAttributes() << id << typeStr << shiftedOrder << basinId );
140 if ( !junctionSink->addFeature( feat, QgsFeatureSink::FastInsert ) )
141 {
142 throw QgsProcessingException( writeFeatureError( junctionSink, parameters, QString() ) );
143 }
144 else
145 {
146 feedback->featureAddedToSink( u"JUNCTIONS"_s );
147 }
148 }
149 };
150
151 multiStepFeedback.setProgressText( QObject::tr( "Calculating junction nodes and seeding basins" ) );
152 multiStepFeedback.setCurrentStep( 0 );
153 for ( int row = 0; row < height; ++row )
154 {
155 if ( feedback->isCanceled() )
156 return;
157
158 multiStepFeedback.setProgress( static_cast<double>( row ) / height );
159
160 for ( int col = 0; col < width; ++col )
161 {
162 const qgssize idx = static_cast<qgssize>( row ) * width + col;
163 const int order = strahlerOrders[idx];
164 if ( order >= threshold )
165 {
166 const int dir = d8Directions[idx];
167 if ( dir >= 0 )
168 {
169 int neighborColumn = 0;
170 int neighborRow = 0;
171 if ( QgsRasterAnalysisUtils::neighborCellCoordinates( dir, row, col, neighborRow, neighborColumn, height, width ) )
172 {
173 const qgssize neighborIdx = static_cast<qgssize>( neighborRow ) * width + neighborColumn;
174 if ( nodesGrid[neighborIdx] == 0 && strahlerOrders[neighborIdx] > order && d8Directions[neighborIdx] >= 0 )
175 {
176 setNode( neighborColumn, neighborRow, ++nNodes, NodeType::Junction, strahlerOrders[neighborIdx], 0 );
177
178 if ( subbasins )
179 {
180 for ( int j = 0; j < 8; ++j )
181 {
182 const int oppositeJ = ( j + 4 ) % 8;
183 int jColumn = 0;
184 int jRow = 0;
185 if ( QgsRasterAnalysisUtils::neighborCellCoordinates( oppositeJ, neighborRow, neighborColumn, jRow, jColumn, height, width ) )
186 {
187 const qgssize jIdx = static_cast<qgssize>( jRow ) * width + jColumn;
188 if ( d8Directions[jIdx] == j && strahlerOrders[jIdx] >= threshold )
189 {
190 basinsGrid[jIdx] = ++nBasins;
191 setNode( jColumn, jRow, 0, NodeType::Mouth, strahlerOrders[jIdx], nBasins );
192 }
193 }
194 }
195 }
196 }
197 }
198
199 if ( order == threshold )
200 {
201 bool isSpring = true;
202 for ( int j = 0; j < 8 && isSpring; ++j )
203 {
204 const int oppositeJ = ( j + 4 ) % 8;
205 int jColumn = 0;
206 int jRow = 0;
207 if ( QgsRasterAnalysisUtils::neighborCellCoordinates( oppositeJ, row, col, jRow, jColumn, height, width ) )
208 {
209 const qgssize jIdx = static_cast<qgssize>( jRow ) * width + jColumn;
210 if ( d8Directions[jIdx] == j )
211 {
212 isSpring = strahlerOrders[jIdx] < threshold;
213 }
214 }
215 }
216
217 if ( isSpring )
218 {
219 setNode( col, row, ++nNodes, NodeType::Spring, order, 0 );
220 }
221 }
222 }
223 else
224 {
225 basinsGrid[idx] = ++nBasins;
226 setNode( col, row, ++nNodes, NodeType::Outlet, order, nBasins );
227 }
228 }
229 }
230 }
231
232 multiStepFeedback.setProgressText( QObject::tr( "Calculating drainage basins" ) );
233 multiStepFeedback.setCurrentStep( 1 );
234 auto getBasin = [&]( int startColumn, int startRow ) {
235 int currentColumn = startColumn;
236 int currentRow = startRow;
237 qgssize currentIdx = static_cast<qgssize>( currentRow ) * width + currentColumn;
238 int basin = basinsGrid[currentIdx];
239
240 if ( basin < 0 )
241 {
242 std::vector<std::size_t> stack;
243 while ( basin < 0 )
244 {
245 const int dir = d8Directions[currentIdx];
246 if ( dir < 0 )
247 break;
248
249 stack.push_back( currentIdx );
250
251 int neighborColumn = 0;
252 int neighborRow = 0;
253 if ( !QgsRasterAnalysisUtils::neighborCellCoordinates( dir, currentRow, currentColumn, neighborRow, neighborColumn, height, width ) )
254 break;
255
256 currentColumn = neighborColumn;
257 currentRow = neighborRow;
258 currentIdx = static_cast<qgssize>( currentRow ) * width + currentColumn;
259 basin = basinsGrid[currentIdx];
260 }
261
262 if ( basin < 0 )
263 {
264 // not linked to any basin, mark as processed!
265 basin = 0;
266 }
267
268 if ( stack.empty() )
269 {
270 basinsGrid[currentIdx] = basin;
271 }
272 for ( const std::size_t idx : stack )
273 {
274 basinsGrid[idx] = basin;
275 }
276 }
277 return basin;
278 };
279
280 for ( int row = 0; row < height; ++row )
281 {
282 if ( feedback->isCanceled() )
283 return;
284
285 multiStepFeedback.setProgress( static_cast<double>( row ) / height );
286
287 for ( int col = 0; col < width; ++col )
288 {
289 getBasin( col, row );
290 }
291 }
292
293 if ( feedback->isCanceled() )
294 return;
295
296 multiStepFeedback.setProgressText( QObject::tr( "Extracting basins as polygons" ) );
297 multiStepFeedback.setCurrentStep( 2 );
298 if ( basinSink )
299 {
300 // create an in-memory GDAL raster dataset for the basins grid
301 GDALDriverH hMemDriver = GDALGetDriverByName( "MEM" );
302 if ( !hMemDriver )
303 throw QgsProcessingException( QObject::tr( "GDAL MEM driver is unavailable." ) );
304
305 GDALDatasetH hMemDS = GDALCreate( hMemDriver, "", width, height, 1, GDT_Int32, nullptr );
306 if ( !hMemDS )
307 throw QgsProcessingException( QObject::tr( "Could not create in-memory GDAL dataset for basin polygonization." ) );
308
309 double adfGeoTransform[6] = { extent.xMinimum(), cellWidth, 0.0, extent.yMaximum(), 0.0, -cellHeight };
310 GDALSetGeoTransform( hMemDS, adfGeoTransform );
311
312 GDALRasterBandH hBand = GDALGetRasterBand( hMemDS, 1 );
313 GDALSetRasterNoDataValue( hBand, 0 );
314
315 const CPLErr writeErr = GDALRasterIO( hBand, GF_Write, 0, 0, width, height, const_cast<int32_t *>( basinsGrid.data() ), width, height, GDT_Int32, 0, 0 );
316 if ( writeErr != CE_None )
317 {
318 GDALClose( hMemDS );
319 throw QgsProcessingException( QObject::tr( "Failed to write basin raster buffer to GDAL dataset." ) );
320 }
321
322 // create an in-memory OGR layer to hold the polygonized output features
323 GDALDriverH hOgrMemDriver = GDALGetDriverByName( "Memory" );
324 if ( !hOgrMemDriver )
325 {
326 GDALClose( hMemDS );
327 throw QgsProcessingException( QObject::tr( "OGR Memory driver is unavailable." ) );
328 }
329
330 GDALDatasetH hOgrDS = GDALCreate( hOgrMemDriver, "", 0, 0, 0, GDT_Unknown, nullptr );
331 OGRLayerH hLayer = GDALDatasetCreateLayer( hOgrDS, "basins", nullptr, wkbPolygon, nullptr );
332
333 // create a "VALUE" field to receive the raster basin ID
334 OGRFieldDefnH hFieldDefn = OGR_Fld_Create( "VALUE", OFTInteger );
335 OGR_L_CreateField( hLayer, hFieldDefn, TRUE );
336 OGR_Fld_Destroy( hFieldDefn );
337
338 // handoff to GDALPolygonize to do the actual raster to vector logic
339
340 struct GdalProgressData
341 {
342 QgsProcessingFeedback *feedback = nullptr;
343 } progressData { &multiStepFeedback };
344
345 auto gdalProgressCallback = []( double dfComplete, const char *, void *pProgressArg ) -> int CPL_STDCALL {
346 if ( pProgressArg )
347 {
348 GdalProgressData *data = static_cast<GdalProgressData *>( pProgressArg );
349 if ( data->feedback )
350 {
351 if ( data->feedback->isCanceled() )
352 return FALSE;
353
354 data->feedback->setProgress( dfComplete );
355 }
356 }
357 return TRUE;
358 };
359
360 char **papszOptions = nullptr;
361 papszOptions = CSLSetNameValue( papszOptions, "8CONNECTED", "8" );
362
363 const CPLErr polyErr = GDALPolygonize( hBand, nullptr, hLayer, 0, papszOptions, gdalProgressCallback, &progressData );
364 CSLDestroy( papszOptions );
365
366 if ( polyErr != CE_None || feedback->isCanceled() )
367 {
368 GDALClose( hOgrDS );
369 GDALClose( hMemDS );
370 throw QgsProcessingException( QObject::tr( "GDAL Polygonize failed during basin vectorization." ) );
371 }
372
373 const GIntBig totalFeatures = OGR_L_GetFeatureCount( hLayer, TRUE );
374 GIntBig featureIdx = 0;
375
376 // get features from ogr memory layer
377 OGR_L_ResetReading( hLayer );
378 OGRFeatureH hFeat = nullptr;
379 multiStepFeedback.setCurrentStep( 3 );
380 while ( ( hFeat = OGR_L_GetNextFeature( hLayer ) ) != nullptr )
381 {
382 featureIdx++;
383 if ( feedback->isCanceled() )
384 {
385 OGR_F_Destroy( hFeat );
386 GDALClose( hOgrDS );
387 GDALClose( hMemDS );
388 return;
389 }
390
391 multiStepFeedback.setProgress( static_cast<double>( featureIdx ) / totalFeatures );
392
393 const int basinId = OGR_F_GetFieldAsInteger( hFeat, 0 );
394 // explicitly ignore no data pixels
395 if ( basinId > 0 )
396 {
397 OGRGeometryH hGeom = OGR_F_GetGeometryRef( hFeat );
398 if ( hGeom )
399 {
401 if ( !qGeom.isEmpty() )
402 {
403 const double area = qGeom.area();
404 const double perimeter = qGeom.length();
405 const int basinOrder = basinToOrderMap.value( basinId, 0 );
406
407 QgsFeature feat;
408 feat.setGeometry( qGeom );
409 feat.setAttributes( QgsAttributes() << basinId << area << perimeter << basinOrder );
410 basinSink->addFeature( feat, QgsFeatureSink::FastInsert );
411 }
412 }
413 }
414 OGR_F_Destroy( hFeat );
415 }
416
417 GDALClose( hOgrDS );
418 GDALClose( hMemDS );
419 }
420
421 multiStepFeedback.setProgressText( QObject::tr( "Vectorizing channel lines" ) );
422 multiStepFeedback.setCurrentStep( 4 );
423 if ( channelSink )
424 {
425 int segmentCount = 0;
426 for ( int row = 0; row < height; ++row )
427 {
428 if ( feedback->isCanceled() )
429 return;
430
431 multiStepFeedback.setProgress( static_cast<double>( row ) / height );
432
433 for ( int column = 0; column < width; ++column )
434 {
435 const qgssize idx = static_cast<qgssize>( row ) * width + column;
436 if ( nodesGrid[idx] > 0 )
437 {
438 int currentColumn = column;
439 int currentRow = row;
440 std::size_t currentIdx = idx;
441 int dir = d8Directions[currentIdx];
442 if ( dir >= 0 )
443 {
444 const int nodeA = nodesGrid[idx];
445 const int basin = basinsGrid[idx];
446 const int rawOrder = strahlerOrders[idx];
447 const int shiftedOrder = rawOrder + 1 - threshold;
448
449 auto line = std::make_unique<QgsLineString>();
450
451 double xWorld;
452 double yWorld;
453 QgsRasterAnalysisUtils::pixelToMap( currentColumn, currentRow, extent, cellWidth, cellHeight, xWorld, yWorld );
454 double z = demBlock->value( currentRow, currentColumn );
455 line->addVertex( QgsPoint( xWorld, yWorld, z ) );
456
457 int nodeB = 0;
458 while ( dir >= 0 )
459 {
460 int neighborColumn = 0;
461 int neighborRow = 0;
462 if ( !QgsRasterAnalysisUtils::neighborCellCoordinates( dir, currentRow, currentColumn, neighborRow, neighborColumn, height, width ) )
463 break;
464
465 currentColumn = neighborColumn;
466 currentRow = neighborRow;
467 currentIdx = static_cast<qgssize>( currentRow ) * width + currentColumn;
468
469 QgsRasterAnalysisUtils::pixelToMap( currentColumn, currentRow, extent, cellWidth, cellHeight, xWorld, yWorld );
470 z = demBlock->value( currentRow, currentColumn );
471 line->addVertex( QgsPoint( xWorld, yWorld, z ) );
472
473 if ( nodesGrid[currentIdx] > 0 )
474 {
475 nodeB = nodesGrid[currentIdx];
476 break;
477 }
478
479 dir = d8Directions[currentIdx];
480 }
481
482 QgsGeometry lineGeom( std::move( line ) );
483 const double length = lineGeom.length();
484
485 QgsFeature feat;
486 feat.setGeometry( lineGeom );
487 feat.setAttributes( QgsAttributes() << segmentCount++ << nodeA << nodeB << basin << shiftedOrder << rawOrder << length );
488 if ( !channelSink->addFeature( feat, QgsFeatureSink::FastInsert ) )
489 {
490 throw QgsProcessingException( writeFeatureError( channelSink, parameters, QString() ) );
491 }
492 else
493 {
494 feedback->featureAddedToSink( u"CHANNELS"_s );
495 }
496 }
497 }
498 }
499 }
500 }
501}
502
503QgsFields QgsChannelNetworkAlgorithmBase::channelFields()
504{
505 QgsFields channelFields;
506 channelFields.append( QgsField( u"SEGMENT_ID"_s, QMetaType::Type::Int ) );
507 channelFields.append( QgsField( u"NODE_A"_s, QMetaType::Type::Int ) );
508 channelFields.append( QgsField( u"NODE_B"_s, QMetaType::Type::Int ) );
509 channelFields.append( QgsField( u"BASIN"_s, QMetaType::Type::Int ) );
510 channelFields.append( QgsField( u"ORDER"_s, QMetaType::Type::Int ) );
511 channelFields.append( QgsField( u"ORDER_CELL"_s, QMetaType::Type::Int ) );
512 channelFields.append( QgsField( u"LENGTH"_s, QMetaType::Type::Double ) );
513 return channelFields;
514}
515
516QgsFields QgsChannelNetworkAlgorithmBase::basinFields()
517{
518 QgsFields basinFields;
519 basinFields.append( QgsField( u"VALUE"_s, QMetaType::Type::Int ) );
520 basinFields.append( QgsField( u"AREA"_s, QMetaType::Type::Double ) );
521 basinFields.append( QgsField( u"PERIMETER"_s, QMetaType::Type::Double ) );
522 basinFields.append( QgsField( u"ORDER"_s, QMetaType::Type::Int ) );
523 return basinFields;
524}
525
526QgsFields QgsChannelNetworkAlgorithmBase::junctionFields()
527{
528 QgsFields junctionFields;
529 junctionFields.append( QgsField( u"ID"_s, QMetaType::Type::Int ) );
530 junctionFields.append( QgsField( u"TYPE"_s, QMetaType::Type::QString ) );
531 junctionFields.append( QgsField( u"ORDER"_s, QMetaType::Type::Int ) );
532 junctionFields.append( QgsField( u"BASIN"_s, QMetaType::Type::Int ) );
533 return junctionFields;
534}
535
536//
537// QgsChannelNetworkFromDemAlgorithm
538//
539
540QString QgsChannelNetworkFromDemAlgorithm::name() const
541{
542 return u"channelnetworkfromdem"_s;
543}
544
545QString QgsChannelNetworkFromDemAlgorithm::displayName() const
546{
547 return QObject::tr( "Channel network and drainage basins from DEM" );
548}
549
550QString QgsChannelNetworkFromDemAlgorithm::shortDescription() const
551{
552 return QObject::tr( "Calculates channel network lines, drainage basin polygons, and junction nodes directly from an elevation raster (DEM)." );
553}
554
555QString QgsChannelNetworkFromDemAlgorithm::shortHelpString() const
556{
557 return QObject::tr(
558 "This algorithm extracts vector channel network lines, drainage basin polygons, and topological junction node points directly from an elevation raster (DEM).\n\n"
559 "The analysis executes a 3-step pipeline:\n"
560 "1. D8 Flow Routing: Computes single-direction steepest descent flow directions.\n"
561 "2. Strahler Stream Ordering: Calculates topological stream orders.\n"
562 "3. Vector Network Extraction: Traces vector channels, delineates catchments, and identifies key topological junction nodes.\n\n"
563 "The output Junctions layer contains topological nodes from the channel network. These are classified according to type:\n"
564 "• Spring: Channel headwater initiation point matching the stream order threshold.\n"
565 "• Junction: Tributary confluence point where two or more stream channels meet.\n"
566 "• Outlet: Terminal discharge node exiting the raster boundary or draining into a terrain sink.\n"
567 "• Mouth: Confluence pour point entering a higher-order stream segment (delineated when subbasins are enabled).\n\n"
568 "This algorithm is a port of SAGA's 'Channel Network and Drainage Basins' tool."
569 );
570}
571
572void QgsChannelNetworkFromDemAlgorithm::initAlgorithm( const QVariantMap & )
573{
574 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT"_s, QObject::tr( "Elevation raster" ) ) );
575 addCommonParameters();
576}
577
578QgsProcessingAlgorithm *QgsChannelNetworkFromDemAlgorithm::createInstance() const
579{
580 return new QgsChannelNetworkFromDemAlgorithm();
581}
582
583bool QgsChannelNetworkFromDemAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
584{
585 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u"INPUT"_s, context );
586 if ( !layer || !layer->dataProvider() )
587 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT"_s ) );
588
589 mDemInterface.reset( layer->dataProvider()->clone() );
590 mLayerWidth = layer->width();
591 mLayerHeight = layer->height();
592 mExtent = layer->extent();
593 mCrs = layer->crs();
594 mCellSizeX = layer->rasterUnitsPerPixelX();
595 mCellSizeY = layer->rasterUnitsPerPixelY();
596
597 return true;
598}
599
600QVariantMap QgsChannelNetworkFromDemAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
601{
602 QGS_MARK_ALGORITHM_SOURCE
603
604 const int threshold = parameterAsInt( parameters, u"THRESHOLD"_s, context );
605 const bool subbasins = parameterAsBool( parameters, u"SUBBASINS"_s, context );
606
607 std::unique_ptr<QgsRasterBlock> demBlock( mDemInterface->block( 1, mExtent, mLayerWidth, mLayerHeight ) );
608 if ( !demBlock )
609 throw QgsProcessingException( QObject::tr( "Could not read input DEM block." ) );
610
611 const qgssize totalCells = static_cast<qgssize>( mLayerWidth ) * mLayerHeight;
612 QgsProcessingMultiStepFeedback multiStepFeedback( 3, feedback );
613 multiStepFeedback.setStepWeights( { 1, 1, 10 } );
614
615 multiStepFeedback.setCurrentStep( 0 );
616 multiStepFeedback.setProgressText( QObject::tr( "Calculating flow direction" ) );
617 std::vector<int8_t> d8Directions( totalCells, -1 );
618 for ( int row = 0; row < mLayerHeight; ++row )
619 {
620 if ( multiStepFeedback.isCanceled() )
621 return {};
622
623 multiStepFeedback.setProgress( 100.0 * static_cast<double>( row ) / mLayerHeight );
624 const qgssize rowOffset = static_cast<qgssize>( row ) * mLayerWidth;
625 for ( int col = 0; col < mLayerWidth; ++col )
626 {
627 const int dir = QgsRasterAnalysisUtils::steepestGradientDirection( demBlock.get(), row, col, mCellSizeX, mCellSizeY, true, true );
628 d8Directions[rowOffset + col] = static_cast<int8_t>( dir );
629 }
630 }
631
632 multiStepFeedback.setProgressText( QObject::tr( "Calculating Strahler stream orders" ) );
633 multiStepFeedback.setCurrentStep( 1 );
634 std::vector<int16_t> strahlerOrders( totalCells, 0 );
635 computeStrahlerOrder( demBlock.get(), d8Directions, mLayerWidth, mLayerHeight, 1, strahlerOrders.data(), &multiStepFeedback, -1 );
636 if ( multiStepFeedback.isCanceled() )
637 return {};
638
639 multiStepFeedback.setCurrentStep( 2 );
640
641 QString channelsDest;
642 std::unique_ptr<QgsFeatureSink> channelSink;
643 if ( !QgsVariantUtils::isNull( parameters.value( u"CHANNELS"_s ) ) )
644 {
645 channelSink.reset( parameterAsSink( parameters, u"CHANNELS"_s, context, channelsDest, channelFields(), Qgis::WkbType::LineStringZ, mCrs ) );
646 if ( !channelSink )
647 {
648 throw QgsProcessingException( invalidSinkError( parameters, u"CHANNELS"_s ) );
649 }
650 }
651
652 QString basinsDest;
653 std::unique_ptr<QgsFeatureSink> basinSink;
654 if ( !QgsVariantUtils::isNull( parameters.value( u"BASINS"_s ) ) )
655 {
656 basinSink.reset( parameterAsSink( parameters, u"BASINS"_s, context, basinsDest, basinFields(), Qgis::WkbType::Polygon, mCrs ) );
657 if ( !basinSink )
658 {
659 throw QgsProcessingException( invalidSinkError( parameters, u"BASINS"_s ) );
660 }
661 }
662
663 QString junctionsDest;
664 std::unique_ptr<QgsFeatureSink> junctionSink;
665 if ( !QgsVariantUtils::isNull( parameters.value( u"JUNCTIONS"_s ) ) )
666 {
667 junctionSink.reset( parameterAsSink( parameters, u"JUNCTIONS"_s, context, junctionsDest, junctionFields(), Qgis::WkbType::PointZ, mCrs ) );
668 if ( !junctionSink )
669 {
670 throw QgsProcessingException( invalidSinkError( parameters, u"JUNCTIONS"_s ) );
671 }
672 }
673
674 extractChannelNetwork( demBlock.get(), d8Directions, strahlerOrders, mLayerWidth, mLayerHeight, mExtent, mCrs, threshold, subbasins, channelSink.get(), basinSink.get(), junctionSink.get(), &multiStepFeedback, parameters );
675
676 if ( channelSink )
677 {
678 channelSink->finalize();
679 feedback->featureSinkFinalized( u"CHANNELS"_s );
680 }
681 if ( basinSink )
682 {
683 basinSink->finalize();
684 feedback->featureSinkFinalized( u"BASINS"_s );
685 }
686 if ( junctionSink )
687 {
688 junctionSink->finalize();
689 feedback->featureSinkFinalized( u"JUNCTIONS"_s );
690 }
691
692 QVariantMap outputs;
693 if ( channelSink )
694 outputs.insert( u"CHANNELS"_s, channelsDest );
695 if ( basinSink )
696 outputs.insert( u"BASINS"_s, basinsDest );
697 if ( junctionSink )
698 outputs.insert( u"JUNCTIONS"_s, junctionsDest );
699
700 return outputs;
701}
702
703//
704// QgsChannelNetworkFromFlowDirAndOrderAlgorithm
705//
706QString QgsChannelNetworkFromFlowDirAndOrderAlgorithm::name() const
707{
708 return u"channelnetworkfromflowdirandorder"_s;
709}
710
711QString QgsChannelNetworkFromFlowDirAndOrderAlgorithm::displayName() const
712{
713 return QObject::tr( "Channel network and drainage basins from multiple inputs" );
714}
715
716QString QgsChannelNetworkFromFlowDirAndOrderAlgorithm::shortDescription() const
717{
718 return QObject::tr( "Calculates channel network lines, drainage basin polygons, and junction nodes using DEM, flow direction, and Strahler order rasters." );
719}
720
721QString QgsChannelNetworkFromFlowDirAndOrderAlgorithm::shortHelpString() const
722{
723 return QObject::tr(
724 "This algorithm extracts vector channel network lines, drainage basin polygons, and topological junction node points using pre-computed elevation (DEM), D8 flow direction, and Strahler stream "
725 "order rasters.\n\n"
726 "This variant bypasses internal raster flow routing and stream order generation, making it ideal when flow direction and Strahler order rasters have already been computed in prior processing "
727 "steps.\n\n"
728 "The output Junctions layer contains topological nodes from the channel network. These are classified according to type:\n"
729 "• Spring: Channel headwater initiation point matching the stream order threshold.\n"
730 "• Junction: Tributary confluence point where two or more stream channels meet.\n"
731 "• Outlet: Terminal discharge node exiting the raster boundary or draining into a terrain sink.\n"
732 "• Mouth: Confluence pour point entering a higher-order stream segment (delineated when subbasins are enabled).\n\n"
733 "This algorithm is a port of SAGA's 'Channel Network and Drainage Basins' tool."
734 );
735}
736
737void QgsChannelNetworkFromFlowDirAndOrderAlgorithm::initAlgorithm( const QVariantMap & )
738{
739 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT_DEM"_s, QObject::tr( "Elevation raster" ) ) );
740 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT_FLOW_DIR"_s, QObject::tr( "Flow direction raster" ) ) );
741 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT_STRAHLER"_s, QObject::tr( "Strahler order raster" ) ) );
742 addCommonParameters();
743}
744
745QgsProcessingAlgorithm *QgsChannelNetworkFromFlowDirAndOrderAlgorithm::createInstance() const
746{
747 return new QgsChannelNetworkFromFlowDirAndOrderAlgorithm();
748}
749
750bool QgsChannelNetworkFromFlowDirAndOrderAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
751{
752 QgsRasterLayer *demLayer = parameterAsRasterLayer( parameters, u"INPUT_DEM"_s, context );
753 if ( !demLayer || !demLayer->dataProvider() )
754 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT_DEM"_s ) );
755
756 QgsRasterLayer *flowDirLayer = parameterAsRasterLayer( parameters, u"INPUT_FLOW_DIR"_s, context );
757 if ( !flowDirLayer || !flowDirLayer->dataProvider() )
758 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT_FLOW_DIR"_s ) );
759
760 QgsRasterLayer *strahlerLayer = parameterAsRasterLayer( parameters, u"INPUT_STRAHLER"_s, context );
761 if ( !strahlerLayer || !strahlerLayer->dataProvider() )
762 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT_STRAHLER"_s ) );
763
764 mDemInterface.reset( demLayer->dataProvider()->clone() );
765 mFlowDirInterface.reset( flowDirLayer->dataProvider()->clone() );
766 mStrahlerInterface.reset( strahlerLayer->dataProvider()->clone() );
767 mLayerWidth = demLayer->width();
768 mLayerHeight = demLayer->height();
769 mExtent = demLayer->extent();
770 mCrs = demLayer->crs();
771
772 return true;
773}
774
775QVariantMap QgsChannelNetworkFromFlowDirAndOrderAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
776{
777 QGS_MARK_ALGORITHM_SOURCE
778
779 const int threshold = parameterAsInt( parameters, u"THRESHOLD"_s, context );
780 const bool subbasins = parameterAsBool( parameters, u"SUBBASINS"_s, context );
781
782 std::unique_ptr<QgsRasterBlock> demBlock( mDemInterface->block( 1, mExtent, mLayerWidth, mLayerHeight ) );
783 if ( !demBlock )
784 throw QgsProcessingException( QObject::tr( "Could not read input DEM block." ) );
785
786 std::unique_ptr<QgsRasterBlock> flowDirBlock( mFlowDirInterface->block( 1, mExtent, mLayerWidth, mLayerHeight ) );
787 if ( !flowDirBlock )
788 throw QgsProcessingException( QObject::tr( "Could not read input flow direction block." ) );
789
790 std::unique_ptr<QgsRasterBlock> strahlerBlock( mStrahlerInterface->block( 1, mExtent, mLayerWidth, mLayerHeight ) );
791 if ( !strahlerBlock )
792 throw QgsProcessingException( QObject::tr( "Could not read input Strahler order block." ) );
793
794 const qgssize totalCells = static_cast<qgssize>( mLayerWidth ) * mLayerHeight;
795
796 std::vector<int8_t> d8Directions( totalCells, -1 );
797 std::vector<int16_t> strahlerOrders( totalCells, 0 );
798
799 for ( int row = 0; row < mLayerHeight; ++row )
800 {
801 if ( feedback->isCanceled() )
802 return {};
803
804 const qgssize rowOffset = static_cast<qgssize>( row ) * mLayerWidth;
805 for ( int col = 0; col < mLayerWidth; ++col )
806 {
807 const std::size_t idx = rowOffset + col;
808 if ( !flowDirBlock->isNoData( row, col ) )
809 {
810 d8Directions[idx] = static_cast<int8_t>( flowDirBlock->value( row, col ) );
811 }
812 if ( !strahlerBlock->isNoData( row, col ) )
813 {
814 strahlerOrders[idx] = static_cast<int16_t>( strahlerBlock->value( row, col ) );
815 }
816 }
817 }
818
819 QString channelsDest;
820 std::unique_ptr<QgsFeatureSink> channelSink;
821 if ( !QgsVariantUtils::isNull( parameters.value( u"CHANNELS"_s ) ) )
822 {
823 channelSink.reset( parameterAsSink( parameters, u"CHANNELS"_s, context, channelsDest, channelFields(), Qgis::WkbType::LineStringZ, mCrs ) );
824 if ( !channelSink )
825 {
826 throw QgsProcessingException( invalidSinkError( parameters, u"CHANNELS"_s ) );
827 }
828 }
829
830 QString basinsDest;
831 std::unique_ptr<QgsFeatureSink> basinSink;
832 if ( !QgsVariantUtils::isNull( parameters.value( u"BASINS"_s ) ) )
833 {
834 basinSink.reset( parameterAsSink( parameters, u"BASINS"_s, context, basinsDest, basinFields(), Qgis::WkbType::Polygon, mCrs ) );
835 if ( !basinSink )
836 {
837 throw QgsProcessingException( invalidSinkError( parameters, u"BASINS"_s ) );
838 }
839 }
840
841 QString junctionsDest;
842 std::unique_ptr<QgsFeatureSink> junctionSink;
843 if ( !QgsVariantUtils::isNull( parameters.value( u"JUNCTIONS"_s ) ) )
844 {
845 junctionSink.reset( parameterAsSink( parameters, u"JUNCTIONS"_s, context, junctionsDest, junctionFields(), Qgis::WkbType::PointZ, mCrs ) );
846 if ( !junctionSink )
847 {
848 throw QgsProcessingException( invalidSinkError( parameters, u"JUNCTIONS"_s ) );
849 }
850 }
851
852 extractChannelNetwork( demBlock.get(), d8Directions, strahlerOrders, mLayerWidth, mLayerHeight, mExtent, mCrs, threshold, subbasins, channelSink.get(), basinSink.get(), junctionSink.get(), feedback, parameters );
853
854 if ( channelSink )
855 {
856 channelSink->finalize();
857 feedback->featureSinkFinalized( u"CHANNELS"_s );
858 }
859 if ( basinSink )
860 {
861 basinSink->finalize();
862 feedback->featureSinkFinalized( u"BASINS"_s );
863 }
864 if ( junctionSink )
865 {
866 junctionSink->finalize();
867 feedback->featureSinkFinalized( u"JUNCTIONS"_s );
868 }
869
870 QVariantMap outputs;
871 if ( channelSink )
872 outputs.insert( u"CHANNELS"_s, channelsDest );
873 if ( basinSink )
874 outputs.insert( u"BASINS"_s, basinsDest );
875 if ( junctionSink )
876 outputs.insert( u"JUNCTIONS"_s, junctionsDest );
877
878 return outputs;
879}
880
@ VectorPoint
Vector point layers.
Definition qgis.h:3752
@ VectorPolygon
Vector polygon layers.
Definition qgis.h:3754
@ VectorLine
Vector line layers.
Definition qgis.h:3753
@ Polygon
Polygon.
Definition qgis.h:298
@ PointZ
PointZ.
Definition qgis.h:313
@ LineStringZ
LineStringZ.
Definition qgis.h:314
A vector of attributes.
Represents a coordinate reference system (CRS).
An interface for objects which accept features via addFeature(s) methods.
virtual bool addFeature(QgsFeature &feature, QgsFeatureSink::Flags flags=QgsFeatureSink::Flags())
Adds a single feature to the sink.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:56
Container of fields for a vector layer.
Definition qgsfields.h:45
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
Definition qgsfields.cpp:75
A geometry is the spatial representation of a feature.
double length() const
Returns the planar, 2-dimensional length of geometry.
double area() const
Returns the planar, 2-dimensional area of the geometry.
bool isEmpty() const
Returns true if the geometry is empty (eg a linestring with no vertices, or a collection with no geom...
virtual Q_INVOKABLE QgsRectangle extent() const
Returns the extent of the layer.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:90
static QgsGeometry ogrGeometryToQgsGeometry(OGRGeometryH geom)
Converts an OGR geometry representation to a QgsGeometry object.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
Abstract base class for processing algorithms.
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.
void featureAddedToSink(const QString &output)
Reports that a feature was added to the the sink associated with the specified algorithm output.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
Processing feedback object for multi-step operations.
A raster layer parameter for processing algorithms.
A vector layer destination parameter, for specifying the destination path for a vector layer created ...
Raster data container.
double value(int row, int column) const
Read a single value if type of block is numeric.
QByteArray data() const
Gets access to raw data.
bool isNoData(int row, int column) const
Checks if value at position is no data.
QgsRasterDataProvider * clone() const override=0
Clone itself, create deep copy.
Represents a raster layer.
int height() const
Returns the height of the (unclipped) raster.
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.
A rectangle specified with double values.
double xMinimum
double yMaximum
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
unsigned long long qgssize
Qgssize is used instead of size_t, because size_t is stdlib type, unknown by SIP, and it would be har...
Definition qgis.h:8310
void * GDALDatasetH