diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 2c6fba66ab7a1..699116c7591a5 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -118,6 +118,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmflattenrelationships.cpp processing/qgsalgorithmforcerhr.cpp processing/qgsalgorithmfuzzifyraster.cpp + processing/qgsalgorithmgenerateelevationprofile.cpp processing/qgsalgorithmgeometrybyexpression.cpp processing/qgsalgorithmgltftovector.cpp processing/qgsalgorithmgpsbabeltools.cpp diff --git a/src/analysis/processing/qgsalgorithmgenerateelevationprofile.cpp b/src/analysis/processing/qgsalgorithmgenerateelevationprofile.cpp new file mode 100644 index 0000000000000..005c7ea032a6a --- /dev/null +++ b/src/analysis/processing/qgsalgorithmgenerateelevationprofile.cpp @@ -0,0 +1,299 @@ +/*************************************************************************** + qgsalgorithmgenerateelevationprofile.cpp + --------------------- + begin : October 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmgenerateelevationprofile.h" + +#include "qgis.h" +#include "qgsabstractprofilesource.h" +#include "qgstextformat.h" +#include "qgsfillsymbol.h" +#include "qgsfillsymbollayer.h" +#include "qgslinesymbol.h" +#include "qgslinesymbollayer.h" +#include "qgsplot.h" +#include "qgsprofilerequest.h" +#include "qgsterrainprovider.h" +#include "qgscurve.h" + +///@cond PRIVATE + +class QgsAlgorithmElevationProfilePlotItem: public Qgs2DPlot +{ + public: + + explicit QgsAlgorithmElevationProfilePlotItem( int width, int height, int dpi ) + : mDpi( dpi ) + { + setYMinimum( 0 ); + setYMaximum( 10 ); + setSize( QSizeF( width, height ) ); + } + + void setRenderer( QgsProfilePlotRenderer *renderer ) + { + mRenderer = renderer; + } + + QRectF plotArea() + { + if ( !mPlotArea.isNull() ) + { + return mPlotArea; + } + + // calculate plot area + QgsRenderContext context; + context.setScaleFactor( mDpi / 25.4 ); + + calculateOptimisedIntervals( context ); + mPlotArea = interiorPlotArea( context ); + return mPlotArea; + } + + void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override + { + mPlotArea = plotArea; + + if ( !mRenderer ) + return; + + rc.painter()->translate( mPlotArea.left(), mPlotArea.top() ); + const QStringList sourceIds = mRenderer->sourceIds(); + for ( const QString &source : sourceIds ) + { + mRenderer->render( rc, mPlotArea.width(), mPlotArea.height(), xMinimum(), xMaximum(), yMinimum(), yMaximum(), source ); + } + rc.painter()->translate( -mPlotArea.left(), -mPlotArea.top() ); + } + + private: + + int mDpi = 96; + QRectF mPlotArea; + QgsProfilePlotRenderer *mRenderer = nullptr; +}; + +void QgsGenerateElevationProfileAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterGeometry( QStringLiteral( "CURVE" ), QObject::tr( "Profile curve" ), QVariant(), false, QList() << static_cast( Qgis::GeometryType::Line ) ) ); + addParameter( new QgsProcessingParameterCrs( QStringLiteral( "CURVE_CRS" ), QObject::tr( "Profile curve CRS" ), QVariant(), false ) ); + addParameter( new QgsProcessingParameterMultipleLayers( QStringLiteral( "MAP_LAYERS" ), QObject::tr( "Map layers" ), Qgis::ProcessingSourceType::MapLayer, QVariant(), false ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "WIDTH" ), QObject::tr( "Chart width" ), Qgis::ProcessingNumberParameterType::Integer, 400, false, 0 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "HEIGHT" ), QObject::tr( "Chart height" ), Qgis::ProcessingNumberParameterType::Integer, 300, false, 0 ) ); + addParameter( new QgsProcessingParameterMapLayer( QStringLiteral( "TERRAIN_LAYER" ), QObject::tr( "Terrain layer" ), QVariant(), true, QList() << static_cast( Qgis::ProcessingSourceType::Raster ) << static_cast( Qgis::ProcessingSourceType::Mesh ) ) ); + + auto textColorParam = std::make_unique< QgsProcessingParameterColor >( QStringLiteral( "TEXT_COLOR" ), QObject::tr( "Chart text color" ), QColor( 0, 0, 0 ), true, true ); + textColorParam->setFlags( textColorParam->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( textColorParam.release() ); + + auto backgroundColorParam = std::make_unique< QgsProcessingParameterColor >( QStringLiteral( "BACKGROUND_COLOR" ), QObject::tr( "Chart background color" ), QColor( 255, 255, 255 ), true, true ); + backgroundColorParam->setFlags( backgroundColorParam->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( backgroundColorParam.release() ); + + auto borderColorParam = std::make_unique< QgsProcessingParameterColor >( QStringLiteral( "BORDER_COLOR" ), QObject::tr( "Chart border color" ), QColor( 99, 99, 99 ), true, true ); + borderColorParam->setFlags( borderColorParam->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( borderColorParam.release() ); + + auto toleranceParam = std::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "TOLERANCE" ), QObject::tr( "Profile tolerance" ), Qgis::ProcessingNumberParameterType::Double, 5.0, true, 0 ); + toleranceParam->setFlags( toleranceParam->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( toleranceParam.release() ); + + auto dpiParam = std::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "DPI" ), QObject::tr( "Chart DPI" ), Qgis::ProcessingNumberParameterType::Integer, 96, true, 0 ); + dpiParam->setFlags( dpiParam->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( dpiParam.release() ); + + addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Output image" ) ) ); +} + +QString QgsGenerateElevationProfileAlgorithm::name() const +{ + return QStringLiteral( "generateelevationprofileimage" ); +} + +QString QgsGenerateElevationProfileAlgorithm::displayName() const +{ + return QObject::tr( "Generate elevation profile image" ); +} + +QStringList QgsGenerateElevationProfileAlgorithm::tags() const +{ + return QObject::tr( "altitude,elevation,terrain,dem" ).split( ',' ); +} + +QString QgsGenerateElevationProfileAlgorithm::group() const +{ + return QObject::tr( "Plots" ); +} + +QString QgsGenerateElevationProfileAlgorithm::groupId() const +{ + return QStringLiteral( "plots" ); +} + +QString QgsGenerateElevationProfileAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm creates an elevation profile image from a list of map layer and an optional terrain." ); +} + +QgsGenerateElevationProfileAlgorithm *QgsGenerateElevationProfileAlgorithm::createInstance() const +{ + return new QgsGenerateElevationProfileAlgorithm(); +} + +bool QgsGenerateElevationProfileAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + const QgsGeometry curveGeom = parameterAsGeometry( parameters, QStringLiteral( "CURVE" ), context ); + const QgsCoordinateReferenceSystem curveCrs = parameterAsCrs( parameters, QStringLiteral( "CURVE_CRS" ), context ); + + QList layers = parameterAsLayerList( parameters, QStringLiteral( "MAP_LAYERS" ), context ); + QgsMapLayer *terrainLayer = parameterAsLayer( parameters, QStringLiteral( "TERRAIN_LAYER" ), context ); + + const double tolerance = parameterAsDouble( parameters, QStringLiteral( "TOLERANCE" ), context ); + + QList sources; + for ( QgsMapLayer *layer : layers ) + { + if ( QgsAbstractProfileSource *source = dynamic_cast( layer ) ) + sources.append( source ); + } + + QgsProfileRequest request( static_cast( curveGeom.constGet()->clone() ) ); + request.setCrs( curveCrs ); + request.setTolerance( tolerance ); + request.setTransformContext( context.transformContext() ); + request.setExpressionContext( context.expressionContext() ); + + if ( terrainLayer ) + { + if ( QgsRasterLayer *rasterLayer = dynamic_cast( terrainLayer ) ) + { + std::unique_ptr terrainProvider = std::make_unique(); + terrainProvider->setLayer( rasterLayer ); + request.setTerrainProvider( terrainProvider.release() ); + } + else if ( QgsMeshLayer *meshLayer = dynamic_cast( terrainLayer ) ) + { + std::unique_ptr terrainProvider = std::make_unique(); + terrainProvider->setLayer( meshLayer ); + request.setTerrainProvider( terrainProvider.release() ); + } + } + + + mRenderer = std::make_unique( sources, request ); + + return true; +} + +QVariantMap QgsGenerateElevationProfileAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + const QgsGeometry curveGeom = parameterAsGeometry( parameters, QStringLiteral( "CURVE" ), context ); + + const int width = parameterAsDouble( parameters, QStringLiteral( "WIDTH" ), context ); + const int height = parameterAsDouble( parameters, QStringLiteral( "HEIGHT" ), context ); + const int dpi = parameterAsDouble( parameters, QStringLiteral( "DPI" ), context ); + + const QString outputImage = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context ); + + const QColor textColor = parameterAsColor( parameters, QStringLiteral( "TEXT_COLOR" ), context ); + const QColor backgroundColor = parameterAsColor( parameters, QStringLiteral( "BACKGROUND_COLOR" ), context ); + const QColor borderColor = parameterAsColor( parameters, QStringLiteral( "BORDER_COLOR" ), context ); + + QgsAlgorithmElevationProfilePlotItem plotItem( width, height, dpi ); + + if ( textColor.isValid() ) + { + QgsTextFormat textFormat = plotItem.xAxis().textFormat(); + textFormat.setColor( textColor ); + plotItem.xAxis().setTextFormat( textFormat ); + textFormat = plotItem.yAxis().textFormat(); + textFormat.setColor( textColor ); + plotItem.yAxis().setTextFormat( textFormat ); + } + + if ( borderColor.isValid() ) + { + std::unique_ptr lineSymbolLayer = std::make_unique( borderColor, 0.1 ); + lineSymbolLayer->setPenCapStyle( Qt::FlatCap ); + plotItem.xAxis().setGridMinorSymbol( new QgsLineSymbol( QgsSymbolLayerList( { lineSymbolLayer->clone() } ) ) ); + plotItem.yAxis().setGridMinorSymbol( new QgsLineSymbol( QgsSymbolLayerList( { lineSymbolLayer->clone() } ) ) ); + plotItem.xAxis().setGridMajorSymbol( new QgsLineSymbol( QgsSymbolLayerList( { lineSymbolLayer->clone() } ) ) ); + plotItem.yAxis().setGridMajorSymbol( new QgsLineSymbol( QgsSymbolLayerList( { lineSymbolLayer->clone() } ) ) ); + plotItem.setChartBorderSymbol( new QgsFillSymbol( QgsSymbolLayerList( { lineSymbolLayer.release() } ) ) ); + } + + if ( backgroundColor.isValid() ) + { + std::unique_ptr fillSymbolLayer = std::make_unique( backgroundColor, Qt::SolidPattern, backgroundColor ); + plotItem.setChartBackgroundSymbol( new QgsFillSymbol( QgsSymbolLayerList( { fillSymbolLayer.release() } ) ) ); + } + + QgsProfileGenerationContext generationContext; + generationContext.setDpi( dpi ); + generationContext.setMaximumErrorMapUnits( MAX_ERROR_PIXELS * ( curveGeom.constGet()->length() ) / plotItem.plotArea().width() ); + generationContext.setMapUnitsPerDistancePixel( curveGeom.constGet()->length() / plotItem.plotArea().width() ); + + mRenderer->setContext( generationContext ); + + mRenderer->startGeneration(); + mRenderer->waitForFinished(); + + const QgsDoubleRange zRange = mRenderer->zRange(); + if ( zRange.upper() < zRange.lower() ) + { + // invalid range, e.g. no features found in plot! + plotItem.setYMinimum( 0 ); + plotItem.setYMaximum( 10 ); + } + else if ( qgsDoubleNear( zRange.lower(), zRange.upper(), 0.0000001 ) ) + { + // corner case ... a zero height plot! Just pick an arbitrary +/- 5 height range. + plotItem.setYMinimum( zRange.lower() - 5 ); + plotItem.setYMaximum( zRange.lower() + 5 ); + } + else + { + // add 5% margin to height range + const double margin = ( zRange.upper() - zRange.lower() ) * 0.05; + plotItem.setYMinimum( zRange.lower() - margin ); + plotItem.setYMaximum( zRange.upper() + margin ); + } + + plotItem.setXMinimum( 0 ); + plotItem.setXMaximum( curveGeom.constGet()->length() ); + + plotItem.setRenderer( mRenderer.get() ); + + QImage image( static_cast( plotItem.size().width() ), static_cast( plotItem.size().height() ), QImage::Format_ARGB32_Premultiplied ); + image.fill( Qt::transparent ); + + QPainter painter( &image ); + painter.setRenderHint( QPainter::Antialiasing, true ); + QgsRenderContext renderContext = QgsRenderContext::fromQPainter( &painter ); + renderContext.setScaleFactor( dpi / 25.4 ); + renderContext.setExpressionContext( context.expressionContext() ); + plotItem.calculateOptimisedIntervals( renderContext ); + plotItem.render( renderContext ); + painter.end(); + image.save( outputImage ); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), outputImage ); + return outputs; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmgenerateelevationprofile.h b/src/analysis/processing/qgsalgorithmgenerateelevationprofile.h new file mode 100644 index 0000000000000..15a68bb4b7ddb --- /dev/null +++ b/src/analysis/processing/qgsalgorithmgenerateelevationprofile.h @@ -0,0 +1,66 @@ +/*************************************************************************** + qgsalgorithmgenerateelevationprofile.h + --------------------- + begin : October 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMGENERATEELEVATIONPROFILE_H +#define QGSALGORITHMGENERATEELEVATIONPROFILE_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" +#include "qgsprofilerenderer.h" +#include "qgsapplication.h" + +class QgsProfilePlotRenderer; + +///@cond PRIVATE + +/** + * Native generate elevation profile image algorithm. + */ +class QgsGenerateElevationProfileAlgorithm : public QgsProcessingAlgorithm +{ + + public: + + QgsGenerateElevationProfileAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsGenerateElevationProfileAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + bool prepareAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + private: + + std::unique_ptr mRenderer; + + static constexpr double MAX_ERROR_PIXELS = 2; +}; + +///@endcond + +#endif // QGSALGORITHMGENERATEELEVATIONPROFILE_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index a74cdfc700b32..febd84d8b4810 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -99,6 +99,7 @@ #include "qgsalgorithmflattenrelationships.h" #include "qgsalgorithmforcerhr.h" #include "qgsalgorithmfuzzifyraster.h" +#include "qgsalgorithmgenerateelevationprofile.h" #include "qgsalgorithmgeometrybyexpression.h" #include "qgsalgorithmgltftovector.h" #if QT_CONFIG(process) @@ -383,6 +384,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsFuzzifyRasterSmallMembershipAlgorithm() ); addAlgorithm( new QgsFuzzifyRasterGaussianMembershipAlgorithm() ); addAlgorithm( new QgsFuzzifyRasterNearMembershipAlgorithm() ); + addAlgorithm( new QgsGenerateElevationProfileAlgorithm() ); addAlgorithm( new QgsGeometryByExpressionAlgorithm() ); addAlgorithm( new QgsGltfToVectorFeaturesAlgorithm() ); #if QT_CONFIG(process)