QGIS API Documentation 4.3.0-Master (ffcfc20b9b4)
Loading...
Searching...
No Matches
qgsalgorithmhypsometriccurves.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmhypsometriccurves.cpp
3 ---------------------
4 begin : April 2026
5 copyright : (C) 2026 by Alexander Bruy
6 email : alexander dot bruy 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
22#include <QString>
23
24using namespace Qt::StringLiterals;
25
27
28QString QgsHypsometricCurvesAlgorithm::name() const
29{
30 return u"hypsometriccurves"_s;
31}
32
33QString QgsHypsometricCurvesAlgorithm::displayName() const
34{
35 return QObject::tr( "Hypsometric curves" );
36}
37
38QStringList QgsHypsometricCurvesAlgorithm::tags() const
39{
40 return QObject::tr( "dem,hypsometric,curves,area,elevation,distribution" ).split( ',' );
41}
42
43QString QgsHypsometricCurvesAlgorithm::group() const
44{
45 return QObject::tr( "Raster terrain analysis" );
46}
47
48QString QgsHypsometricCurvesAlgorithm::groupId() const
49{
50 return u"rasterterrainanalysis"_s;
51}
52
53QString QgsHypsometricCurvesAlgorithm::shortHelpString() const
54{
55 return QObject::tr(
56 "This algorithm computes hypsometric curves for an input Digital Elevation Model (DEM) "
57 "clipped to the boundaries of features from a vector layer.\n\n"
58 "A hypsometric curve is a cumulative distribution function of elevations within a "
59 "geographic area. It illustrates the relationship between altitude and area, providing "
60 "insight into the geomorphological maturity of a catchment or landscape. The curve "
61 "plots the total area (either absolute or percentage) found at or below a specific elevation.\n\n"
62 "The algorithm generates one CSV file for each feature in the boundary layer. These "
63 "files contain two columns: the cumulative area and the corresponding elevation level.\n\n"
64 "Note: Only raster pixels whose centroids are located within the boundary of a feature are "
65 "considered in the calculation for that feature."
66 );
67}
68
69QString QgsHypsometricCurvesAlgorithm::shortDescription() const
70{
71 return QObject::tr( "Computes hypsometric curves for an input Digital Elevation Model (DEM) as table files." );
72}
73
74QgsHypsometricCurvesAlgorithm *QgsHypsometricCurvesAlgorithm::createInstance() const
75{
76 return new QgsHypsometricCurvesAlgorithm();
77}
78
79void QgsHypsometricCurvesAlgorithm::initAlgorithm( const QVariantMap & )
80{
81 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT_DEM"_s, QObject::tr( "DEM to analyze" ) ) );
82 addParameter( new QgsProcessingParameterFeatureSource( u"BOUNDARY_LAYER"_s, QObject::tr( "Boundary layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorPolygon ) ) );
83 auto stepParam = std::make_unique<QgsProcessingParameterNumber>( u"STEP"_s, QObject::tr( "Step" ), Qgis::ProcessingNumberParameterType::Double, 100, false, 0 );
84 stepParam->setHelp( QObject::tr( "The vertical interval (elevation increment) used to group data. A smaller step results in a smoother, more detailed curve." ) );
85 addParameter( stepParam.release() );
86 addParameter( new QgsProcessingParameterBoolean( u"USE_PERCENTAGE"_s, QObject::tr( "Use % of area instead of absolute value" ), false ) );
87 addParameter( new QgsProcessingParameterFolderDestination( u"OUTPUT_DIRECTORY"_s, QObject::tr( "Hypsometric curves" ), QVariant(), true ) );
88 addParameter( new QgsProcessingParameterFeatureSink( u"OUTPUT"_s, QObject::tr( "Hypsometric curves" ), Qgis::ProcessingSourceType::Vector, QVariant(), true, false ) );
89}
90
91bool QgsHypsometricCurvesAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
92{
93 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u"INPUT_DEM"_s, context );
94 if ( !layer )
95 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT_DEM"_s ) );
96
97 mHasNoDataValue = layer->dataProvider()->sourceHasNoDataValue( 1 );
98 mNodataValue = layer->dataProvider()->sourceNoDataValue( 1 );
99 mRasterInterface.reset( layer->dataProvider()->clone() );
100 mRasterExtent = layer->extent();
101 mCrs = layer->crs();
102 mCellSizeX = std::abs( layer->rasterUnitsPerPixelX() );
103 mCellSizeY = std::abs( layer->rasterUnitsPerPixelY() );
104 mNbCellsXProvider = mRasterInterface->xSize();
105 mNbCellsYProvider = mRasterInterface->ySize();
106
107 mStep = parameterAsDouble( parameters, u"STEP"_s, context );
108 mUsePercentage = parameterAsBool( parameters, u"USE_PERCENTAGE"_s, context );
109
110 return true;
111}
112
113QVariantMap QgsHypsometricCurvesAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
114{
115 QGS_MARK_ALGORITHM_SOURCE
116
117 std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, u"BOUNDARY_LAYER"_s, context ) );
118 if ( !source )
119 {
120 throw QgsProcessingException( invalidSourceError( parameters, u"BOUNDARY_LAYER"_s ) );
121 }
122
123 const QString outputPath = parameterAsString( parameters, u"OUTPUT_DIRECTORY"_s, context );
124 if ( !outputPath.isEmpty() && !QDir().mkpath( outputPath ) )
125 {
126 throw QgsProcessingException( QObject::tr( "Could not create output directory '%1'." ).arg( outputPath ) );
127 }
128
129 QgsFields sourceFields = source->fields();
130 QgsFields newFields = QgsFields();
131 newFields.append( QgsField( u"polygon_id"_s, QMetaType::Type::Int, QString(), 20 ) );
132 newFields.append( QgsField( u"area"_s, QMetaType::Type::Double, QString(), 20, 6 ) );
133 newFields.append( QgsField( u"elevation"_s, QMetaType::Type::Double, QString(), 20, 6 ) );
134
135 const QgsFields outputFields = QgsProcessingUtils::combineFields( sourceFields, newFields );
136
137 QString destId;
138 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, destId, outputFields, Qgis::WkbType::NoGeometry, source->sourceCrs() ) );
139 if ( parameters.value( u"OUTPUT"_s ).isValid() && !sink )
140 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
141
142 const long long featureCount = source->featureCount();
143 const double progressStep = featureCount > 0 ? 100.0 / static_cast<double>( featureCount ) : 0.0;
144 long long current = 0;
145
146 QgsFeatureRequest request;
147 request.setNoAttributes();
148 if ( source->sourceCrs() != mCrs )
149 {
150 request.setDestinationCrs( mCrs, context.transformContext() );
151 }
152 QgsFeatureIterator it = source->getFeatures( request );
153 QgsFeature f;
154
155 QVector<double> elevations;
156
157 while ( it.nextFeature( f ) )
158 {
159 if ( feedback->isCanceled() )
160 {
161 break;
162 }
163
164 feedback->setProgress( static_cast<double>( current ) * progressStep );
165
166 if ( !f.hasGeometry() )
167 {
168 current++;
169 continue;
170 }
171
172 const QgsGeometry featureGeometry = f.geometry();
173 const QgsRectangle featureRect = featureGeometry.boundingBox().intersect( mRasterExtent );
174 if ( featureRect.isEmpty() )
175 {
176 feedback->pushInfo( QObject::tr( "Feature %1 does not intersect the raster extent." ).arg( f.id() ) );
177 current++;
178 continue;
179 }
180
181 int startCol, startRow, endCol, endRow;
182 QgsRasterAnalysisUtils::mapToPixel( featureRect.xMinimum(), featureRect.yMaximum(), mRasterExtent, mCellSizeX, mCellSizeY, startCol, startRow );
183 QgsRasterAnalysisUtils::mapToPixel( featureRect.xMaximum(), featureRect.yMinimum(), mRasterExtent, mCellSizeX, mCellSizeY, endCol, endRow );
184
185 const int nCellsX = endCol - startCol;
186 const int nCellsY = endRow - startRow;
187
188 if ( nCellsX <= 0 || nCellsY <= 0 )
189 {
190 feedback->pushInfo( QObject::tr( "Feature %1 is smaller than the raster cell size." ).arg( f.id() ) );
191 current++;
192 continue;
193 }
194
195 const QgsRectangle rasterBlockExtent(
196 mRasterExtent.xMinimum() + startCol * mCellSizeX,
197 mRasterExtent.yMaximum() - ( startRow + nCellsY ) * mCellSizeY,
198 mRasterExtent.xMinimum() + ( startCol + nCellsX ) * mCellSizeX,
199 mRasterExtent.yMaximum() - startRow * mCellSizeY
200 );
201
202
203 elevations.clear();
204 elevations.reserve( static_cast<qsizetype>( nCellsX ) * nCellsY );
205
206 auto collectValue = [&]( double value, const QgsPointXY & ) {
207 if ( mHasNoDataValue && qgsDoubleNear( value, mNodataValue ) )
208 {
209 return;
210 }
211 if ( std::isnan( value ) )
212 {
213 return;
214 }
215 elevations.append( value );
216 };
217
218 QgsRasterAnalysisUtils::statisticsFromMiddlePointTest( mRasterInterface.get(), 1, featureGeometry, nCellsX, nCellsY, mCellSizeX, mCellSizeY, rasterBlockExtent, collectValue, true );
219
220 if ( elevations.isEmpty() )
221 {
222 feedback->pushInfo( QObject::tr( "Feature %1 does not intersect the raster or is entirely in a NODATA area." ).arg( f.id() ) );
223 current++;
224 continue;
225 }
226
227 QMap<double, double> hypsometry = calculateHypsometry( elevations, feedback );
228 if ( hypsometry.isEmpty() )
229 {
230 current++;
231 continue;
232 }
233
234 if ( !outputPath.isEmpty() )
235 {
236 const QString csvPath = QDir( outputPath ).filePath( u"histogram_%1_%2.csv"_s.arg( source->sourceName() ).arg( f.id() ) );
237 QFile outFile( csvPath );
238 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
239 {
240 feedback->reportError( QObject::tr( "Could not open output file '%1' for writing." ).arg( csvPath ) );
241 current++;
242 continue;
243 }
244
245 QTextStream stream( &outFile );
246 stream.setEncoding( QStringConverter::Utf8 );
247 stream.setRealNumberNotation( QTextStream::SmartNotation );
248 stream.setRealNumberPrecision( std::numeric_limits<double>::max_digits10 );
249 stream << QObject::tr( "Area" ) << ',' << QObject::tr( "Elevation" ) << '\n';
250
251 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
252 {
253 stream << it.value() << ',' << it.key() << '\n';
254 }
255 outFile.close();
256 }
257
258 if ( sink )
259 {
260 QgsAttributes sourceAttributes = f.attributes();
261 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
262 {
263 QgsFeature feat;
264 QgsAttributes attributes = sourceAttributes;
265 feat.setFields( outputFields );
266 attributes.append( f.id() );
267 attributes.append( it.value() ); // area
268 attributes.append( it.key() ); // elevation
269 feat.setAttributes( attributes );
270 if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) )
271 {
272 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
273 }
274 else
275 {
276 feedback->featureAddedToSink( u"OUTPUT"_s );
277 }
278 }
279 }
280
281 current++;
282 }
283
284 QVariantMap results;
285 if ( !outputPath.isEmpty() )
286 {
287 results.insert( u"OUTPUT_DIRECTORY"_s, outputPath );
288 }
289 if ( sink )
290 {
291 sink->finalize();
292 feedback->featureSinkFinalized( u"OUTPUT"_s );
293 results.insert( u"OUTPUT"_s, destId );
294 }
295 return results;
296}
297
298QMap<double, double> QgsHypsometricCurvesAlgorithm::calculateHypsometry( const QVector<double> &elevations, QgsProcessingFeedback *feedback ) const
299{
300 if ( elevations.isEmpty() )
301 {
302 return {};
303 }
304
305 const auto [minIt, maxIt] = std::minmax_element( elevations.begin(), elevations.end() );
306 const double minValue = *minIt;
307 const double maxValue = *maxIt;
308
309 const qgssize bins = static_cast<qgssize>( std::ceil( ( maxValue - minValue ) / mStep ) );
310 if ( bins > MAX_BINS )
311 {
312 feedback->reportError( QObject::tr( "The combination of elevation range %1 – %2 and step %3 requires too many histograms bins. Please use a larger step value." ).arg( minValue ).arg( maxValue ).arg( mStep ), true );
313 return {};
314 }
315
316 std::map<double, qgssize> histogram;
317 {
318 double startValue = minValue;
319 double tmpValue = minValue + mStep;
320 while ( startValue < maxValue )
321 {
322 // prevent endless loop when adding mStep does not advance tmpValue,
323 // e.g. when tmpValue is very large
324 if ( tmpValue <= startValue )
325 {
326 break;
327 }
328
329 histogram[tmpValue] = 0;
330 startValue = tmpValue;
331 tmpValue += mStep;
332 }
333 }
334
335 for ( const double v : elevations )
336 {
337 auto it = histogram.upper_bound( v );
338 if ( it != histogram.end() )
339 {
340 ++it->second;
341 }
342 }
343
344 const double totalPixels = static_cast<double>( elevations.size() );
345 const double multiplier = mUsePercentage ? 100.0 / totalPixels : mCellSizeX * mCellSizeY;
346
347 // hypsometric data: key = upper bin value, value = area
348 QMap<double, double> data;
349
350 for ( const auto &[key, count] : histogram )
351 {
352 data[key] = static_cast<double>( count ) * multiplier;
353 }
354
355 double cumulative = 0.0;
356 for ( auto &area : data )
357 {
358 cumulative += area;
359 area = cumulative;
360 }
361
362 return data;
363}
364
@ Vector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
Definition qgis.h:3755
@ VectorPolygon
Vector polygon layers.
Definition qgis.h:3752
@ NoGeometry
No geometry.
Definition qgis.h:312
@ Double
Double/float values.
Definition qgis.h:4023
A vector of attributes.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setDestinationCrs(const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context)
Sets the destination crs for feature's geometries.
QgsFeatureRequest & setNoAttributes()
Set that no attributes will be fetched.
@ 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
QgsAttributes attributes
Definition qgsfeature.h:64
QgsFeatureId id
Definition qgsfeature.h:63
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
void setFields(const QgsFields &fields, bool initAttributes=false)
Assigns a field map with the feature to allow attribute access by attribute name.
QgsGeometry geometry
Definition qgsfeature.h:66
bool hasGeometry() const
Returns true if the feature has an associated geometry.
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
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.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
virtual Q_INVOKABLE QgsRectangle extent() const
Returns the extent of the layer.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:90
Represents a 2D point.
Definition qgspointxy.h:62
Contains information about the context in which a processing algorithm is executed.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
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.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
A boolean parameter for processing algorithms.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A folder destination parameter, for specifying the destination path for a folder created by the algor...
A raster layer parameter for processing algorithms.
static QgsFields combineFields(const QgsFields &fieldsA, const QgsFields &fieldsB, const QString &fieldsBPrefix=QString())
Combines two field lists, avoiding duplicate field names (in a case-insensitive manner).
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.
Represents a raster 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.
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
QgsRectangle intersect(const QgsRectangle &rect) const
Returns the intersection with the given rectangle.
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:8136
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7557