24using namespace Qt::StringLiterals;
28QString QgsHypsometricCurvesAlgorithm::name()
const
30 return u
"hypsometriccurves"_s;
33QString QgsHypsometricCurvesAlgorithm::displayName()
const
35 return QObject::tr(
"Hypsometric curves" );
38QStringList QgsHypsometricCurvesAlgorithm::tags()
const
40 return QObject::tr(
"dem,hypsometric,curves,area,elevation,distribution" ).split(
',' );
43QString QgsHypsometricCurvesAlgorithm::group()
const
45 return QObject::tr(
"Raster terrain analysis" );
48QString QgsHypsometricCurvesAlgorithm::groupId()
const
50 return u
"rasterterrainanalysis"_s;
53QString QgsHypsometricCurvesAlgorithm::shortHelpString()
const
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."
69QString QgsHypsometricCurvesAlgorithm::shortDescription()
const
71 return QObject::tr(
"Computes hypsometric curves for an input Digital Elevation Model (DEM) as table files." );
74QgsHypsometricCurvesAlgorithm *QgsHypsometricCurvesAlgorithm::createInstance()
const
76 return new QgsHypsometricCurvesAlgorithm();
79void QgsHypsometricCurvesAlgorithm::initAlgorithm(
const QVariantMap & )
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() );
93 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u
"INPUT_DEM"_s, context );
100 mRasterExtent = layer->
extent();
104 mNbCellsXProvider = mRasterInterface->xSize();
105 mNbCellsYProvider = mRasterInterface->ySize();
107 mStep = parameterAsDouble( parameters, u
"STEP"_s, context );
108 mUsePercentage = parameterAsBool( parameters, u
"USE_PERCENTAGE"_s, context );
115 std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, u
"BOUNDARY_LAYER"_s, context ) );
121 const QString outputPath = parameterAsString( parameters, u
"OUTPUT_DIRECTORY"_s, context );
122 if ( !outputPath.isEmpty() && !QDir().mkpath( outputPath ) )
124 throw QgsProcessingException( QObject::tr(
"Could not create output directory '%1'." ).arg( outputPath ) );
127 QgsFields sourceFields = source->fields();
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 ) );
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 )
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;
146 if ( source->sourceCrs() != mCrs )
153 QVector<double> elevations;
162 feedback->
setProgress(
static_cast<double>( current ) * progressStep );
174 feedback->
pushInfo( QObject::tr(
"Feature %1 does not intersect the raster extent." ).arg( f.
id() ) );
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 );
183 const int nCellsX = endCol - startCol;
184 const int nCellsY = endRow - startRow;
186 if ( nCellsX <= 0 || nCellsY <= 0 )
188 feedback->
pushInfo( QObject::tr(
"Feature %1 is smaller than the raster cell size." ).arg( f.
id() ) );
194 mRasterExtent.xMinimum() + startCol * mCellSizeX,
195 mRasterExtent.yMaximum() - ( startRow + nCellsY ) * mCellSizeY,
196 mRasterExtent.xMinimum() + ( startCol + nCellsX ) * mCellSizeX,
197 mRasterExtent.yMaximum() - startRow * mCellSizeY
202 elevations.reserve(
static_cast<qsizetype
>( nCellsX ) * nCellsY );
204 auto collectValue = [&](
double value,
const QgsPointXY & ) {
205 if ( mHasNoDataValue &&
qgsDoubleNear( value, mNodataValue ) )
209 elevations.append( value );
212 QgsRasterAnalysisUtils::statisticsFromMiddlePointTest( mRasterInterface.get(), 1, featureGeometry, nCellsX, nCellsY, mCellSizeX, mCellSizeY, rasterBlockExtent, collectValue,
true );
214 if ( elevations.isEmpty() )
216 feedback->
pushInfo( QObject::tr(
"Feature %1 does not intersect the raster or is entirely in a NODATA area." ).arg( f.
id() ) );
221 QMap<double, double> hypsometry = calculateHypsometry( elevations, feedback );
222 if ( hypsometry.isEmpty() )
228 if ( !outputPath.isEmpty() )
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 ) )
234 feedback->
reportError( QObject::tr(
"Could not open output file '%1' for writing." ).arg( csvPath ) );
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';
245 for (
auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
247 stream << it.value() <<
',' << it.key() <<
'\n';
255 for (
auto it = hypsometry.cbegin(); it != hypsometry.cend(); ++it )
260 attributes.append( f.
id() );
261 attributes.append( it.value() );
262 attributes.append( it.key() );
275 if ( !outputPath.isEmpty() )
277 results.insert( u
"OUTPUT_DIRECTORY"_s, outputPath );
281 results.insert( u
"OUTPUT"_s, destId );
286QMap<double, double> QgsHypsometricCurvesAlgorithm::calculateHypsometry(
const QVector<double> &elevations,
QgsProcessingFeedback *feedback )
const
288 if ( elevations.isEmpty() )
293 const auto [minIt, maxIt] = std::minmax_element( elevations.begin(), elevations.end() );
294 const double minValue = *minIt;
295 const double maxValue = *maxIt;
297 const qgssize bins =
static_cast<qgssize>( std::ceil( ( maxValue - minValue ) / mStep ) );
298 if ( bins > MAX_BINS )
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 );
304 std::map<double, qgssize> histogram;
306 double startValue = minValue;
307 double tmpValue = minValue + mStep;
308 while ( startValue < maxValue )
312 if ( tmpValue <= startValue )
317 histogram[tmpValue] = 0;
318 startValue = tmpValue;
323 for (
const double v : elevations )
325 auto it = histogram.upper_bound( v );
326 if ( it != histogram.end() )
332 const double totalPixels =
static_cast<double>( elevations.size() );
333 const double multiplier = mUsePercentage ? 100.0 / totalPixels : mCellSizeX * mCellSizeY;
336 QMap<double, double> data;
338 for (
const auto &[key, count] : histogram )
340 data[key] =
static_cast<double>( count ) * multiplier;
343 double cumulative = 0.0;
344 for (
auto &area : data )
@ Vector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
@ VectorPolygon
Vector polygon layers.
@ Double
Double/float values.
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...
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.
bool hasGeometry() const
Returns true if the feature has an associated geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
void setProgress(double progress)
Sets the current progress for the feedback object.
Encapsulate a field in an attribute table or data source.
Container of fields for a vector layer.
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
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
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.
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...
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).