QGIS API Documentation 4.1.0-Master (ca2ac17535b)
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 if ( std::isnan( value ) )
210 {
211 return;
212 }
213 elevations.append( value );
214 };
215
216 QgsRasterAnalysisUtils::statisticsFromMiddlePointTest( mRasterInterface.get(), 1, featureGeometry, nCellsX, nCellsY, mCellSizeX, mCellSizeY, rasterBlockExtent, collectValue, true );
217
218 if ( elevations.isEmpty() )
219 {
220 feedback->pushInfo( QObject::tr( "Feature %1 does not intersect the raster or is entirely in a NODATA area." ).arg( f.id() ) );
221 current++;
222 continue;
223 }
224
225 QMap<double, double> hypsometry = calculateHypsometry( elevations, feedback );
226 if ( hypsometry.isEmpty() )
227 {
228 current++;
229 continue;
230 }
231
232 if ( !outputPath.isEmpty() )
233 {
234 const QString csvPath = QDir( outputPath ).filePath( u"histogram_%1_%2.csv"_s.arg( source->sourceName() ).arg( f.id() ) );
235 QFile outFile( csvPath );
236 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
237 {
238 feedback->reportError( QObject::tr( "Could not open output file '%1' for writing." ).arg( csvPath ) );
239 current++;
240 continue;
241 }
242
243 QTextStream stream( &outFile );
244 stream.setEncoding( QStringConverter::Utf8 );
245 stream.setRealNumberNotation( QTextStream::SmartNotation );
246 stream.setRealNumberPrecision( std::numeric_limits<double>::max_digits10 );
247 stream << QObject::tr( "Area" ) << ',' << QObject::tr( "Elevation" ) << '\n';
248
249 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
250 {
251 stream << it.value() << ',' << it.key() << '\n';
252 }
253 outFile.close();
254 }
255
256 if ( sink )
257 {
258 QgsAttributes sourceAttributes = f.attributes();
259 for ( auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
260 {
261 QgsFeature feat;
262 QgsAttributes attributes = sourceAttributes;
263 feat.setFields( outputFields );
264 attributes.append( f.id() );
265 attributes.append( it.value() ); // area
266 attributes.append( it.key() ); // elevation
267 feat.setAttributes( attributes );
268 if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) )
269 {
270 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
271 }
272 else
273 {
274 feedback->featureAddedToSink( u"OUTPUT"_s );
275 }
276 }
277 }
278
279 current++;
280 }
281
282 QVariantMap results;
283 if ( !outputPath.isEmpty() )
284 {
285 results.insert( u"OUTPUT_DIRECTORY"_s, outputPath );
286 }
287 if ( sink )
288 {
289 sink->finalize();
290 feedback->featureSinkFinalized( u"OUTPUT"_s );
291 results.insert( u"OUTPUT"_s, destId );
292 }
293 return results;
294}
295
296QMap<double, double> QgsHypsometricCurvesAlgorithm::calculateHypsometry( const QVector<double> &elevations, QgsProcessingFeedback *feedback ) const
297{
298 if ( elevations.isEmpty() )
299 {
300 return {};
301 }
302
303 const auto [minIt, maxIt] = std::minmax_element( elevations.begin(), elevations.end() );
304 const double minValue = *minIt;
305 const double maxValue = *maxIt;
306
307 const qgssize bins = static_cast<qgssize>( std::ceil( ( maxValue - minValue ) / mStep ) );
308 if ( bins > MAX_BINS )
309 {
310 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 );
311 return {};
312 }
313
314 std::map<double, qgssize> histogram;
315 {
316 double startValue = minValue;
317 double tmpValue = minValue + mStep;
318 while ( startValue < maxValue )
319 {
320 // prevent endless loop when adding mStep does not advance tmpValue,
321 // e.g. when tmpValue is very large
322 if ( tmpValue <= startValue )
323 {
324 break;
325 }
326
327 histogram[tmpValue] = 0;
328 startValue = tmpValue;
329 tmpValue += mStep;
330 }
331 }
332
333 for ( const double v : elevations )
334 {
335 auto it = histogram.upper_bound( v );
336 if ( it != histogram.end() )
337 {
338 ++it->second;
339 }
340 }
341
342 const double totalPixels = static_cast<double>( elevations.size() );
343 const double multiplier = mUsePercentage ? 100.0 / totalPixels : mCellSizeX * mCellSizeY;
344
345 // hypsometric data: key = upper bin value, value = area
346 QMap<double, double> data;
347
348 for ( const auto &[key, count] : histogram )
349 {
350 data[key] = static_cast<double>( count ) * multiplier;
351 }
352
353 double cumulative = 0.0;
354 for ( auto &area : data )
355 {
356 cumulative += area;
357 area = cumulative;
358 }
359
360 return data;
361}
362
@ 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: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: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.
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:7761
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7236