QGIS API Documentation 4.1.0-Master (376402f9aeb)
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 std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, u"BOUNDARY_LAYER"_s, context ) );
116 if ( !source )
117 {
118 throw QgsProcessingException( invalidSourceError( parameters, u"BOUNDARY_LAYER"_s ) );
119 }
120
121 const QString outputPath = parameterAsString( parameters, u"OUTPUT_DIRECTORY"_s, context );
122 if ( !outputPath.isEmpty() && !QDir().mkpath( outputPath ) )
123 {
124 throw QgsProcessingException( QObject::tr( "Could not create output directory '%1'." ).arg( outputPath ) );
125 }
126
127 QgsFields sourceFields = source->fields();
128 QgsFields newFields = QgsFields();
129 newFields.append( QgsField( u"polygon_id"_s, QMetaType::Type::Int, QString(), 20 ) );
130 newFields.append( QgsField( u"area"_s, QMetaType::Type::Double, QString(), 20, 6 ) );
131 newFields.append( QgsField( u"elevation"_s, QMetaType::Type::Double, QString(), 20, 6 ) );
132
133 const QgsFields outputFields = QgsProcessingUtils::combineFields( sourceFields, newFields );
134
135 QString destId;
136 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, destId, outputFields, Qgis::WkbType::NoGeometry, source->sourceCrs() ) );
137 if ( parameters.value( u"OUTPUT"_s ).isValid() && !sink )
138 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
139
140 const long long featureCount = source->featureCount();
141 const double progressStep = featureCount > 0 ? 100.0 / static_cast<double>( featureCount ) : 0.0;
142 long long current = 0;
143
144 QgsFeatureRequest request;
145 request.setNoAttributes();
146 if ( source->sourceCrs() != mCrs )
147 {
148 request.setDestinationCrs( mCrs, context.transformContext() );
149 }
150 QgsFeatureIterator it = source->getFeatures( request );
151 QgsFeature f;
152
153 QVector<double> elevations;
154
155 while ( it.nextFeature( f ) )
156 {
157 if ( feedback->isCanceled() )
158 {
159 break;
160 }
161
162 feedback->setProgress( static_cast<double>( current ) * progressStep );
163
164 if ( !f.hasGeometry() )
165 {
166 current++;
167 continue;
168 }
169
170 const QgsGeometry featureGeometry = f.geometry();
171 const QgsRectangle featureRect = featureGeometry.boundingBox().intersect( mRasterExtent );
172 if ( featureRect.isEmpty() )
173 {
174 feedback->pushInfo( QObject::tr( "Feature %1 does not intersect the raster extent." ).arg( f.id() ) );
175 current++;
176 continue;
177 }
178
179 int startCol, startRow, endCol, endRow;
180 QgsRasterAnalysisUtils::mapToPixel( featureRect.xMinimum(), featureRect.yMaximum(), mRasterExtent, mCellSizeX, mCellSizeY, startCol, startRow );
181 QgsRasterAnalysisUtils::mapToPixel( featureRect.xMaximum(), featureRect.yMinimum(), mRasterExtent, mCellSizeX, mCellSizeY, endCol, endRow );
182
183 const int nCellsX = endCol - startCol;
184 const int nCellsY = endRow - startRow;
185
186 if ( nCellsX <= 0 || nCellsY <= 0 )
187 {
188 feedback->pushInfo( QObject::tr( "Feature %1 is smaller than the raster cell size." ).arg( f.id() ) );
189 current++;
190 continue;
191 }
192
193 const QgsRectangle rasterBlockExtent(
194 mRasterExtent.xMinimum() + startCol * mCellSizeX,
195 mRasterExtent.yMaximum() - ( startRow + nCellsY ) * mCellSizeY,
196 mRasterExtent.xMinimum() + ( startCol + nCellsX ) * mCellSizeX,
197 mRasterExtent.yMaximum() - startRow * mCellSizeY
198 );
199
200
201 elevations.clear();
202 elevations.reserve( static_cast<qsizetype>( nCellsX ) * nCellsY );
203
204 auto collectValue = [&]( double value, const QgsPointXY & ) {
205 if ( mHasNoDataValue && qgsDoubleNear( value, mNodataValue ) )
206 {
207 return;
208 }
209 elevations.append( value );
210 };
211
212 QgsRasterAnalysisUtils::statisticsFromMiddlePointTest( mRasterInterface.get(), 1, featureGeometry, nCellsX, nCellsY, mCellSizeX, mCellSizeY, rasterBlockExtent, collectValue, true );
213
214 if ( elevations.isEmpty() )
215 {
216 feedback->pushInfo( QObject::tr( "Feature %1 does not intersect the raster or is entirely in a NODATA area." ).arg( f.id() ) );
217 current++;
218 continue;
219 }
220
221 QMap<double, double> hypsometry = calculateHypsometry( elevations, feedback );
222 if ( hypsometry.isEmpty() )
223 {
224 current++;
225 continue;
226 }
227
228 if ( !outputPath.isEmpty() )
229 {
230 const QString csvPath = QDir( outputPath ).filePath( u"histogram_%1_%2.csv"_s.arg( source->sourceName() ).arg( f.id() ) );
231 QFile outFile( csvPath );
232 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
233 {
234 feedback->reportError( QObject::tr( "Could not open output file '%1' for writing." ).arg( csvPath ) );
235 current++;
236 continue;
237 }
238
239 QTextStream stream( &outFile );
240 stream.setEncoding( QStringConverter::Utf8 );
241 stream.setRealNumberNotation( QTextStream::SmartNotation );
242 stream.setRealNumberPrecision( std::numeric_limits<double>::max_digits10 );
243 stream << QObject::tr( "Area" ) << ',' << QObject::tr( "Elevation" ) << '\n';
244
245 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
246 {
247 stream << it.value() << ',' << it.key() << '\n';
248 }
249 outFile.close();
250 }
251
252 if ( sink )
253 {
254 QgsAttributes sourceAttributes = f.attributes();
255 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
256 {
257 QgsFeature feat;
258 QgsAttributes attributes = sourceAttributes;
259 feat.setFields( outputFields );
260 attributes.append( f.id() );
261 attributes.append( it.value() ); // area
262 attributes.append( it.key() ); // elevation
263 feat.setAttributes( attributes );
264 if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) )
265 {
266 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
267 }
268 }
269 }
270
271 current++;
272 }
273
274 QVariantMap results;
275 if ( !outputPath.isEmpty() )
276 {
277 results.insert( u"OUTPUT_DIRECTORY"_s, outputPath );
278 }
279 if ( sink )
280 {
281 results.insert( u"OUTPUT"_s, destId );
282 }
283 return results;
284}
285
286QMap<double, double> QgsHypsometricCurvesAlgorithm::calculateHypsometry( const QVector<double> &elevations, QgsProcessingFeedback *feedback ) const
287{
288 if ( elevations.isEmpty() )
289 {
290 return {};
291 }
292
293 const auto [minIt, maxIt] = std::minmax_element( elevations.begin(), elevations.end() );
294 const double minValue = *minIt;
295 const double maxValue = *maxIt;
296
297 const qgssize bins = static_cast<qgssize>( std::ceil( ( maxValue - minValue ) / mStep ) );
298 if ( bins > MAX_BINS )
299 {
300 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 );
301 return {};
302 }
303
304 std::map<double, qgssize> histogram;
305 {
306 double startValue = minValue;
307 double tmpValue = minValue + mStep;
308 while ( startValue < maxValue )
309 {
310 // prevent endless loop when adding mStep does not advance tmpValue,
311 // e.g. when tmpValue is very large
312 if ( tmpValue <= startValue )
313 {
314 break;
315 }
316
317 histogram[tmpValue] = 0;
318 startValue = tmpValue;
319 tmpValue += mStep;
320 }
321 }
322
323 for ( const double v : elevations )
324 {
325 auto it = histogram.upper_bound( v );
326 if ( it != histogram.end() )
327 {
328 ++it->second;
329 }
330 }
331
332 const double totalPixels = static_cast<double>( elevations.size() );
333 const double multiplier = mUsePercentage ? 100.0 / totalPixels : mCellSizeX * mCellSizeY;
334
335 // hypsometric data: key = upper bin value, value = area
336 QMap<double, double> data;
337
338 for ( const auto &[key, count] : histogram )
339 {
340 data[key] = static_cast<double>( count ) * multiplier;
341 }
342
343 double cumulative = 0.0;
344 for ( auto &area : data )
345 {
346 cumulative += area;
347 area = cumulative;
348 }
349
350 return data;
351}
352
@ Vector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
Definition qgis.h:3720
@ VectorPolygon
Vector polygon layers.
Definition qgis.h:3717
@ NoGeometry
No geometry.
Definition qgis.h:312
@ Double
Double/float values.
Definition qgis.h:3988
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:69
QgsFeatureId id
Definition qgsfeature.h:68
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:71
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:46
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.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
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:7659
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7134