diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 163e01082b1..150b72de666 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -289,6 +289,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmtaperedbuffer.cpp processing/qgsalgorithmtinmeshcreation.cpp processing/qgsalgorithmtransect.cpp + processing/qgsalgorithmtransectbase.cpp processing/qgsalgorithmtransectfixeddistance.cpp processing/qgsalgorithmtransform.cpp processing/qgsalgorithmtranslate.cpp diff --git a/src/analysis/processing/qgsalgorithmtransect.cpp b/src/analysis/processing/qgsalgorithmtransect.cpp index e634fb4ce56..c41baa5ea0d 100644 --- a/src/analysis/processing/qgsalgorithmtransect.cpp +++ b/src/analysis/processing/qgsalgorithmtransect.cpp @@ -31,40 +31,6 @@ QString QgsTransectAlgorithm::displayName() const return QObject::tr( "Transect" ); } -QStringList QgsTransectAlgorithm::tags() const -{ - return QObject::tr( "transect,station,lines,extend," ).split( ',' ); -} - -QString QgsTransectAlgorithm::group() const -{ - return QObject::tr( "Vector geometry" ); -} - -QString QgsTransectAlgorithm::groupId() const -{ - return QStringLiteral( "vectorgeometry" ); -} - -void QgsTransectAlgorithm::initAlgorithm( const QVariantMap & ) -{ - addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList() << static_cast( Qgis::ProcessingSourceType::VectorLine ) ) ); - auto length = std::make_unique( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), 5.0, QStringLiteral( "INPUT" ), false, 0 ); - length->setIsDynamic( true ); - length->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), QgsPropertyDefinition::DoublePositive ) ); - length->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); - addParameter( length.release() ); - - auto angle = std::make_unique( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees from the original line at the vertices" ), Qgis::ProcessingNumberParameterType::Double, 90.0, false, 0, 360 ); - angle->setIsDynamic( true ); - angle->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees" ), QgsPropertyDefinition::Double ) ); - angle->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); - addParameter( angle.release() ); - - addParameter( new QgsProcessingParameterEnum( QStringLiteral( "SIDE" ), QObject::tr( "Side to create the transects" ), QStringList() << QObject::tr( "Left" ) << QObject::tr( "Right" ) << QObject::tr( "Both" ), false ) ); - addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Transect" ), Qgis::ProcessingSourceType::VectorLine ) ); -} - QString QgsTransectAlgorithm::shortHelpString() const { return QObject::tr( "This algorithm creates transects on vertices for (multi)linestrings.\n" ) @@ -79,156 +45,37 @@ QString QgsTransectAlgorithm::shortHelpString() const + QObject::tr( "- TR_ORIENT: Side of the transect (only on the left or right of the line, or both side)\n" ); } -QString QgsTransectAlgorithm::shortDescription() const -{ - return QObject::tr( "Creates transects on vertices for (multi)linestrings." ); -} - -Qgis::ProcessingAlgorithmDocumentationFlags QgsTransectAlgorithm::documentationFlags() const -{ - return Qgis::ProcessingAlgorithmDocumentationFlag::RegeneratesPrimaryKey; -} - QgsTransectAlgorithm *QgsTransectAlgorithm::createInstance() const { return new QgsTransectAlgorithm(); } -QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +void QgsTransectAlgorithm::addAlgorithmParams() { - const Side orientation = static_cast( parameterAsInt( parameters, QStringLiteral( "SIDE" ), context ) ); - const double angle = fabs( parameterAsDouble( parameters, QStringLiteral( "ANGLE" ), context ) ); - const bool dynamicAngle = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "ANGLE" ) ); - QgsProperty angleProperty; - if ( dynamicAngle ) - angleProperty = parameters.value( QStringLiteral( "ANGLE" ) ).value(); - - double length = parameterAsDouble( parameters, QStringLiteral( "LENGTH" ), context ); - const bool dynamicLength = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "LENGTH" ) ); - QgsProperty lengthProperty; - if ( dynamicLength ) - lengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value(); - - if ( orientation == QgsTransectAlgorithm::Both ) - length /= 2.0; - - std::unique_ptr source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); - if ( !source ) - throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); - - QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast( source.get() ) ); - - QgsFields newFields; - newFields.append( QgsField( QStringLiteral( "TR_FID" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ID" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_SEGMENT" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ANGLE" ), QMetaType::Type::Double, QString(), 5, 2 ) ); - newFields.append( QgsField( QStringLiteral( "TR_LENGTH" ), QMetaType::Type::Double, QString(), 20, 6 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ORIENT" ), QMetaType::Type::Int, QString(), 1 ) ); - QgsFields fields = QgsProcessingUtils::combineFields( source->fields(), newFields ); - - Qgis::WkbType outputWkb = Qgis::WkbType::LineString; - if ( QgsWkbTypes::hasZ( source->wkbType() ) ) - outputWkb = QgsWkbTypes::addZ( outputWkb ); - if ( QgsWkbTypes::hasM( source->wkbType() ) ) - outputWkb = QgsWkbTypes::addM( outputWkb ); - - QString dest; - std::unique_ptr sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, outputWkb, source->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) ); - if ( !sink ) - throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); - - QgsFeatureIterator features = source->getFeatures(); - - int current = -1; - int number = 0; - const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 1; - QgsFeature feat; - - - while ( features.nextFeature( feat ) ) - { - current++; - if ( feedback->isCanceled() ) - { - break; - } - - feedback->setProgress( current * step ); - if ( !feat.hasGeometry() ) - continue; - - QgsGeometry inputGeometry = feat.geometry(); - - if ( dynamicLength || dynamicAngle ) - { - expressionContext.setFeature( feat ); - } - - double evaluatedLength = length; - if ( dynamicLength ) - evaluatedLength = lengthProperty.valueAsDouble( context.expressionContext(), length ); - double evaluatedAngle = angle; - if ( dynamicAngle ) - evaluatedAngle = angleProperty.valueAsDouble( context.expressionContext(), angle ); - - inputGeometry.convertToMultiType(); - const QgsMultiLineString *multiLine = static_cast( inputGeometry.constGet() ); - for ( int id = 0; id < multiLine->numGeometries(); ++id ) - { - const QgsLineString *line = multiLine->lineStringN( id ); - QgsAbstractGeometry::vertex_iterator it = line->vertices_begin(); - while ( it != line->vertices_end() ) - { - const QgsVertexId vertexId = it.vertexId(); - const int i = vertexId.vertex; - QgsFeature outFeat; - QgsAttributes attrs = feat.attributes(); - attrs << current << number << i + 1 << evaluatedAngle << ( ( orientation == QgsTransectAlgorithm::Both ) ? evaluatedLength * 2 : evaluatedLength ) << orientation; - outFeat.setAttributes( attrs ); - const double angleAtVertex = line->vertexAngle( vertexId ); - outFeat.setGeometry( calcTransect( *it, angleAtVertex, evaluatedLength, orientation, evaluatedAngle ) ); - if ( !sink->addFeature( outFeat, QgsFeatureSink::FastInsert ) ) - throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); - number++; - it++; - } - } - } - - sink->finalize(); - - QVariantMap outputs; - outputs.insert( QStringLiteral( "OUTPUT" ), dest ); - return outputs; + // No additional parameters for the basic transect algorithm (vertex-based only) } - -QgsGeometry QgsTransectAlgorithm::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectAlgorithm::Side orientation, const double angle ) +bool QgsTransectAlgorithm::prepareAlgorithmTransectParameters( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) { - QgsPoint pLeft; // left point of the line - QgsPoint pRight; // right point of the line - - QgsPolyline line; - - if ( ( orientation == QgsTransectAlgorithm::Right ) || ( orientation == QgsTransectAlgorithm::Both ) ) - { - pLeft = point.project( length, angle + 180.0 / M_PI * angleAtVertex ); - if ( orientation != QgsTransectAlgorithm::Both ) - pRight = point; - } - - if ( ( orientation == QgsTransectAlgorithm::Left ) || ( orientation == QgsTransectAlgorithm::Both ) ) - { - pRight = point.project( -length, angle + 180.0 / M_PI * angleAtVertex ); - if ( orientation != QgsTransectAlgorithm::Both ) - pLeft = point; - } - - line.append( pLeft ); - line.append( pRight ); - - return QgsGeometry::fromPolyline( line ); + // No additional preparation needed for basic transect algorithm + return true; } -///@endcond +std::vector QgsTransectAlgorithm::generateSamplingPoints( const QgsLineString &line, const QVariantMap &, QgsProcessingContext & ) +{ + std::vector samplingPoints; + + // Vertex-based sampling only (like original master algorithm) + for ( auto it = line.vertices_begin(); it != line.vertices_end(); ++it ) + samplingPoints.push_back( *it ); + + return samplingPoints; +} + +double QgsTransectAlgorithm::calculateAzimuth( const QgsLineString &line, const QgsPoint &, int pointIndex ) +{ + // For vertex-based sampling, use vertex angle directly (like original master algorithm) + return line.vertexAngle( QgsVertexId( 0, 0, pointIndex ) ); +} + +///@endcond \ No newline at end of file diff --git a/src/analysis/processing/qgsalgorithmtransect.h b/src/analysis/processing/qgsalgorithmtransect.h index cee1a67fe4e..8f471c29d15 100644 --- a/src/analysis/processing/qgsalgorithmtransect.h +++ b/src/analysis/processing/qgsalgorithmtransect.h @@ -21,50 +21,27 @@ #define SIP_NO_FILE #include "qgis_sip.h" -#include "qgsprocessingalgorithm.h" +#include "qgsalgorithmtransectbase.h" ///@cond PRIVATE /** * Native transect algorithm. */ -class QgsTransectAlgorithm : public QgsProcessingAlgorithm +class QgsTransectAlgorithm : public QgsTransectAlgorithmBase { public: - /** - * Draw the transect on which side of the line - */ - enum Side - { - Left, - Right, - Both - }; QgsTransectAlgorithm() = 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; - QString shortDescription() const override; - Qgis::ProcessingAlgorithmDocumentationFlags documentationFlags() const override; QgsTransectAlgorithm *createInstance() const override SIP_FACTORY; protected: - QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; - - private: - /** - * Returns the transect of the point \a point with \a length, \a orientation and \a angle. - * \param point The vertex - * \param angleAtVertex Angle at the vertex - * \param length Length of the transect Distance to extend line from input feature - * \param orientation Orientation of the transect - * \param angle Angle of the transect relative to the segment [\a p1 - \a p2] (degrees clockwise) - */ - QgsGeometry calcTransect( const QgsPoint &point, double angleAtVertex, double length, Side orientation, double angle ); + void addAlgorithmParams() override; + bool prepareAlgorithmTransectParameters( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + std::vector generateSamplingPoints( const QgsLineString &line, const QVariantMap ¶meters, QgsProcessingContext &context ) override; + double calculateAzimuth( const QgsLineString &line, const QgsPoint &point, int pointIndex ) override; }; ///@endcond PRIVATE diff --git a/src/analysis/processing/qgsalgorithmtransectbase.cpp b/src/analysis/processing/qgsalgorithmtransectbase.cpp new file mode 100644 index 00000000000..ff7b997d68d --- /dev/null +++ b/src/analysis/processing/qgsalgorithmtransectbase.cpp @@ -0,0 +1,221 @@ +/*************************************************************************** + qgsalgorithmtransectbase.cpp + ---------------------------- + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "qgsalgorithmtransectbase.h" +#include "qgsmultilinestring.h" +#include "qgslinestring.h" + +///@cond PRIVATE + +QString QgsTransectAlgorithmBase::group() const +{ + return QObject::tr( "Vector geometry" ); +} + +QString QgsTransectAlgorithmBase::groupId() const +{ + return QStringLiteral( "vectorgeometry" ); +} + +QStringList QgsTransectAlgorithmBase::tags() const +{ + return QObject::tr( "transect,station,lines,extend" ).split( ',' ); +} + +QString QgsTransectAlgorithmBase::shortDescription() const +{ + return QObject::tr( "Creates transects for (multi)linestrings." ); +} + +Qgis::ProcessingAlgorithmDocumentationFlags QgsTransectAlgorithmBase::documentationFlags() const +{ + return Qgis::ProcessingAlgorithmDocumentationFlag::RegeneratesPrimaryKey; +} + +void QgsTransectAlgorithmBase::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList() << static_cast( Qgis::ProcessingSourceType::VectorLine ) ) ); + + auto length = std::make_unique( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), 5.0, QStringLiteral( "INPUT" ), false, 0 ); + length->setIsDynamic( true ); + length->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), QgsPropertyDefinition::DoublePositive ) ); + length->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); + addParameter( length.release() ); + + auto angle = std::make_unique( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees from the original line at the vertices" ), Qgis::ProcessingNumberParameterType::Double, 90.0, false, 0, 360 ); + angle->setIsDynamic( true ); + angle->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees" ), QgsPropertyDefinition::Double ) ); + angle->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); + addParameter( angle.release() ); + + addParameter( new QgsProcessingParameterEnum( QStringLiteral( "SIDE" ), QObject::tr( "Side to create the transects" ), QStringList() << QObject::tr( "Left" ) << QObject::tr( "Right" ) << QObject::tr( "Both" ), false ) ); + + // Allow subclasses to add their specific parameters + addAlgorithmParams(); + + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Transect" ), Qgis::ProcessingSourceType::VectorLine ) ); +} + +QVariantMap QgsTransectAlgorithmBase::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + mOrientation = static_cast( parameterAsInt( parameters, QStringLiteral( "SIDE" ), context ) ); + mAngle = fabs( parameterAsDouble( parameters, QStringLiteral( "ANGLE" ), context ) ); + mDynamicAngle = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "ANGLE" ) ); + if ( mDynamicAngle ) + mAngleProperty = parameters.value( QStringLiteral( "ANGLE" ) ).value(); + + mLength = parameterAsDouble( parameters, QStringLiteral( "LENGTH" ), context ); + mDynamicLength = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "LENGTH" ) ); + if ( mDynamicLength ) + mLengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value(); + + if ( mOrientation == QgsTransectAlgorithmBase::Both ) + mLength /= 2.0; + + // Let subclass prepare their specific parameters + if ( !prepareAlgorithmTransectParameters( parameters, context, feedback ) ) + return QVariantMap(); + + std::unique_ptr source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast( source.get() ) ); + + QgsFields newFields; + newFields.append( QgsField( QStringLiteral( "TR_FID" ), QMetaType::Type::Int, QString(), 20 ) ); + newFields.append( QgsField( QStringLiteral( "TR_ID" ), QMetaType::Type::Int, QString(), 20 ) ); + newFields.append( QgsField( QStringLiteral( "TR_SEGMENT" ), QMetaType::Type::Int, QString(), 20 ) ); + newFields.append( QgsField( QStringLiteral( "TR_ANGLE" ), QMetaType::Type::Double, QString(), 5, 2 ) ); + newFields.append( QgsField( QStringLiteral( "TR_LENGTH" ), QMetaType::Type::Double, QString(), 20, 6 ) ); + newFields.append( QgsField( QStringLiteral( "TR_ORIENT" ), QMetaType::Type::Int, QString(), 1 ) ); + QgsFields fields = QgsProcessingUtils::combineFields( source->fields(), newFields ); + + Qgis::WkbType outputWkb = Qgis::WkbType::LineString; + if ( QgsWkbTypes::hasZ( source->wkbType() ) ) + outputWkb = QgsWkbTypes::addZ( outputWkb ); + if ( QgsWkbTypes::hasM( source->wkbType() ) ) + outputWkb = QgsWkbTypes::addM( outputWkb ); + + QString dest; + std::unique_ptr sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, outputWkb, source->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) ); + if ( !sink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + + QgsFeatureIterator features = source->getFeatures(); + + int current = -1; + int number = 0; + const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 1; + QgsFeature feat; + + while ( features.nextFeature( feat ) ) + { + current++; + if ( feedback->isCanceled() ) + { + break; + } + + feedback->setProgress( current * step ); + if ( !feat.hasGeometry() ) + continue; + + QgsGeometry inputGeometry = feat.geometry(); + + if ( mDynamicLength || mDynamicAngle ) + { + expressionContext.setFeature( feat ); + } + + double evaluatedLength = mLength; + if ( mDynamicLength ) + evaluatedLength = mLengthProperty.valueAsDouble( context.expressionContext(), mLength ); + double evaluatedAngle = mAngle; + if ( mDynamicAngle ) + evaluatedAngle = mAngleProperty.valueAsDouble( context.expressionContext(), mAngle ); + + inputGeometry.convertToMultiType(); + const QgsMultiLineString *multiLine = static_cast( inputGeometry.constGet() ); + + for ( int part = 0; part < multiLine->numGeometries(); ++part ) + { + const QgsLineString *lineString = multiLine->lineStringN( part ); + if ( !lineString ) + continue; + + QgsLineString line = *lineString; + + // Let subclass generate sampling points using their specific strategy + std::vector samplingPoints = generateSamplingPoints( line, parameters, context ); + + for ( int i = 0; i < static_cast( samplingPoints.size() ); ++i ) + { + const QgsPoint &pt = samplingPoints[i]; + + // Let subclass calculate azimuth using their specific method + double azimuth = calculateAzimuth( line, pt, i ); + + QgsFeature outFeat; + QgsAttributes attrs = feat.attributes(); + attrs << current << number << i + 1 << evaluatedAngle + << ( ( mOrientation == QgsTransectAlgorithmBase::Both ) ? evaluatedLength * 2 : evaluatedLength ) + << static_cast( mOrientation ); + outFeat.setAttributes( attrs ); + outFeat.setGeometry( calcTransect( pt, azimuth, evaluatedLength, mOrientation, evaluatedAngle ) ); + if ( !sink->addFeature( outFeat, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); + number++; + } + } + } + + sink->finalize(); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + return outputs; +} + +QgsGeometry QgsTransectAlgorithmBase::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectAlgorithmBase::Side orientation, const double angle ) +{ + QgsPoint pLeft; // left point of the line + QgsPoint pRight; // right point of the line + + QgsPolyline line; + + if ( ( orientation == QgsTransectAlgorithmBase::Right ) || ( orientation == QgsTransectAlgorithmBase::Both ) ) + { + pLeft = point.project( length, angle + 180.0 / M_PI * angleAtVertex ); + if ( orientation != QgsTransectAlgorithmBase::Both ) + pRight = point; + } + + if ( ( orientation == QgsTransectAlgorithmBase::Left ) || ( orientation == QgsTransectAlgorithmBase::Both ) ) + { + pRight = point.project( -length, angle + 180.0 / M_PI * angleAtVertex ); + if ( orientation != QgsTransectAlgorithmBase::Both ) + pLeft = point; + } + + line.append( pLeft ); + line.append( pRight ); + + return QgsGeometry::fromPolyline( line ); +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmtransectbase.h b/src/analysis/processing/qgsalgorithmtransectbase.h new file mode 100644 index 00000000000..17378aba32f --- /dev/null +++ b/src/analysis/processing/qgsalgorithmtransectbase.h @@ -0,0 +1,97 @@ +/*************************************************************************** + qgsalgorithmtransectbase.h + ------------------------- + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 QGSALGORITHMTRANSECTBASE_H +#define QGSALGORITHMTRANSECTBASE_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Base class for transect algorithms. + */ +class QgsTransectAlgorithmBase : public QgsProcessingAlgorithm +{ + public: + /** + * Draw the transect on which side of the line + */ + enum Side + { + Left, + Right, + Both + }; + + QString group() const final; + QString groupId() const final; + QStringList tags() const override; + QString shortDescription() const override; + Qgis::ProcessingAlgorithmDocumentationFlags documentationFlags() const final; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) final; + + protected: + /** + * Adds specific subclass algorithm parameters. The common parameters (INPUT, LENGTH, ANGLE, SIDE, OUTPUT) + * are automatically added by the base class. + */ + virtual void addAlgorithmParams() = 0; + + /** + * Prepares the transect algorithm subclass for execution. + */ + virtual bool prepareAlgorithmTransectParameters( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) = 0; + + /** + * Processes a line geometry using the specific sampling strategy implemented in subclasses. + */ + QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) final; + + /** + * Pure virtual method that generates sampling points along a line geometry. + * Subclasses implement their specific sampling strategy here. + */ + virtual std::vector generateSamplingPoints( const QgsLineString &line, const QVariantMap ¶meters, QgsProcessingContext &context ) = 0; + + /** + * Calculate the azimuth at a given point for transect orientation. + * Subclasses can override this if they need different azimuth calculation. + */ + virtual double calculateAzimuth( const QgsLineString &line, const QgsPoint &point, int pointIndex ) = 0; + + /** + * Returns the transect geometry at the specified point. + */ + static QgsGeometry calcTransect( const QgsPoint &point, double angleAtVertex, double length, Side orientation, double angle ); + + // Shared member variables accessible to subclasses + Side mOrientation = Both; + double mAngle = 90.0; + double mLength = 5.0; + bool mDynamicAngle = false; + bool mDynamicLength = false; + QgsProperty mAngleProperty; + QgsProperty mLengthProperty; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMTRANSECTBASE_H diff --git a/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp b/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp index fae813759d9..082ea9cc3f3 100644 --- a/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp +++ b/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp @@ -1,9 +1,9 @@ /*************************************************************************** qgsalgorithmtransectfixeddistance.cpp ------------------------------------- - begin : September 2024 - copyright : (C) 2024 by Loïc Bartoletti - email : lbartoletti at tuxfamily dot org + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com ***************************************************************************/ /*************************************************************************** @@ -36,38 +36,6 @@ QStringList QgsTransectFixedDistanceAlgorithm::tags() const return QObject::tr( "transect,station,lines,extend,fixed,interval,distance" ).split( ',' ); } -QString QgsTransectFixedDistanceAlgorithm::group() const -{ - return QObject::tr( "Vector geometry" ); -} - -QString QgsTransectFixedDistanceAlgorithm::groupId() const -{ - return QStringLiteral( "vectorgeometry" ); -} - -void QgsTransectFixedDistanceAlgorithm::initAlgorithm( const QVariantMap & ) -{ - addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList() << static_cast( Qgis::ProcessingSourceType::VectorLine ) ) ); - auto length = std::make_unique( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), 5.0, QStringLiteral( "INPUT" ), false, 0 ); - length->setIsDynamic( true ); - length->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "LENGTH" ), QObject::tr( "Length of the transect" ), QgsPropertyDefinition::DoublePositive ) ); - length->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); - addParameter( length.release() ); - - auto angle = std::make_unique( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees from the original line at the vertices" ), Qgis::ProcessingNumberParameterType::Double, 90.0, false, 0, 360 ); - angle->setIsDynamic( true ); - angle->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "ANGLE" ), QObject::tr( "Angle in degrees" ), QgsPropertyDefinition::Double ) ); - angle->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); - addParameter( angle.release() ); - - addParameter( new QgsProcessingParameterEnum( QStringLiteral( "SIDE" ), QObject::tr( "Side to create the transects" ), QStringList() << QObject::tr( "Left" ) << QObject::tr( "Right" ) << QObject::tr( "Both" ), false ) ); - - addParameter( new QgsProcessingParameterNumber( QStringLiteral( "INTERVAL" ), QObject::tr( "Fixed sampling interval" ), Qgis::ProcessingNumberParameterType::Double, 10.0, false, 0 ) ); - - addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Transect" ), Qgis::ProcessingSourceType::VectorLine ) ); -} - QString QgsTransectFixedDistanceAlgorithm::shortHelpString() const { return QObject::tr( "This algorithm creates transects at fixed distance intervals along (multi)linestrings.\n" ) @@ -87,172 +55,45 @@ QString QgsTransectFixedDistanceAlgorithm::shortDescription() const return QObject::tr( "Creates transects at fixed distance intervals along (multi)linestrings." ); } -Qgis::ProcessingAlgorithmDocumentationFlags QgsTransectFixedDistanceAlgorithm::documentationFlags() const -{ - return Qgis::ProcessingAlgorithmDocumentationFlag::RegeneratesPrimaryKey; -} - QgsTransectFixedDistanceAlgorithm *QgsTransectFixedDistanceAlgorithm::createInstance() const { return new QgsTransectFixedDistanceAlgorithm(); } -QVariantMap QgsTransectFixedDistanceAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +void QgsTransectFixedDistanceAlgorithm::addAlgorithmParams() { - const Side orientation = static_cast( parameterAsInt( parameters, QStringLiteral( "SIDE" ), context ) ); - const double angle = fabs( parameterAsDouble( parameters, QStringLiteral( "ANGLE" ), context ) ); - const bool dynamicAngle = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "ANGLE" ) ); - QgsProperty angleProperty; - if ( dynamicAngle ) - angleProperty = parameters.value( QStringLiteral( "ANGLE" ) ).value(); - - double length = parameterAsDouble( parameters, QStringLiteral( "LENGTH" ), context ); - const bool dynamicLength = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "LENGTH" ) ); - QgsProperty lengthProperty; - if ( dynamicLength ) - lengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value(); - - const double interval = parameterAsDouble( parameters, QStringLiteral( "INTERVAL" ), context ); - - if ( orientation == QgsTransectFixedDistanceAlgorithm::Both ) - length /= 2.0; - - std::unique_ptr source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); - if ( !source ) - throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); - - QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast( source.get() ) ); - - QgsFields newFields; - newFields.append( QgsField( QStringLiteral( "TR_FID" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ID" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_SEGMENT" ), QMetaType::Type::Int, QString(), 20 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ANGLE" ), QMetaType::Type::Double, QString(), 5, 2 ) ); - newFields.append( QgsField( QStringLiteral( "TR_LENGTH" ), QMetaType::Type::Double, QString(), 20, 6 ) ); - newFields.append( QgsField( QStringLiteral( "TR_ORIENT" ), QMetaType::Type::Int, QString(), 1 ) ); - QgsFields fields = QgsProcessingUtils::combineFields( source->fields(), newFields ); - - Qgis::WkbType outputWkb = Qgis::WkbType::LineString; - if ( QgsWkbTypes::hasZ( source->wkbType() ) ) - outputWkb = QgsWkbTypes::addZ( outputWkb ); - if ( QgsWkbTypes::hasM( source->wkbType() ) ) - outputWkb = QgsWkbTypes::addM( outputWkb ); - - QString dest; - std::unique_ptr sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, outputWkb, source->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) ); - if ( !sink ) - throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); - - QgsFeatureIterator features = source->getFeatures(); - - int current = -1; - int number = 0; - const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 1; - QgsFeature feat; - - while ( features.nextFeature( feat ) ) - { - current++; - if ( feedback->isCanceled() ) - { - break; - } - - feedback->setProgress( current * step ); - if ( !feat.hasGeometry() ) - continue; - - QgsGeometry inputGeometry = feat.geometry(); - - if ( dynamicLength || dynamicAngle ) - { - expressionContext.setFeature( feat ); - } - - double evaluatedLength = length; - if ( dynamicLength ) - evaluatedLength = lengthProperty.valueAsDouble( context.expressionContext(), length ); - double evaluatedAngle = angle; - if ( dynamicAngle ) - evaluatedAngle = angleProperty.valueAsDouble( context.expressionContext(), angle ); - - inputGeometry.convertToMultiType(); - const QgsMultiLineString *multiLine = static_cast( inputGeometry.constGet() ); - - for ( int part = 0; part < multiLine->numGeometries(); ++part ) - { - const QgsLineString *lineString = multiLine->lineStringN( part ); - if ( !lineString ) - continue; - - QgsLineString line = *lineString; - std::vector samplingPoints; - - // Sample points at fixed intervals - double totalLength = line.length(); - for ( double d = 0; d <= totalLength; d += interval ) - { - QgsPoint *pt = line.interpolatePoint( d ); - samplingPoints.push_back( *pt ); - } - - for ( int i = 0; i < static_cast( samplingPoints.size() ); ++i ) - { - const QgsPoint &pt = samplingPoints[i]; - double azimuth = 0; - - QgsPoint segPt; - QgsVertexId vid; - line.closestSegment( pt, segPt, vid, nullptr, Qgis::DEFAULT_SEGMENT_EPSILON ); - QgsVertexId prev( vid.part, vid.ring, vid.vertex - 1 ); - azimuth = line.vertexAt( prev ).azimuth( line.vertexAt( vid ) ) * M_PI / 180.0; - - QgsFeature outFeat; - QgsAttributes attrs = feat.attributes(); - attrs << current << number << i + 1 << evaluatedAngle - << ( ( orientation == QgsTransectFixedDistanceAlgorithm::Both ) ? evaluatedLength * 2 : evaluatedLength ) - << static_cast( orientation ); - outFeat.setAttributes( attrs ); - outFeat.setGeometry( calcTransect( pt, azimuth, evaluatedLength, orientation, evaluatedAngle ) ); - if ( !sink->addFeature( outFeat, QgsFeatureSink::FastInsert ) ) - throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); - number++; - } - } - } - - sink->finalize(); - - QVariantMap outputs; - outputs.insert( QStringLiteral( "OUTPUT" ), dest ); - return outputs; + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "INTERVAL" ), QObject::tr( "Fixed sampling interval" ), Qgis::ProcessingNumberParameterType::Double, 10.0, false, 0 ) ); } -QgsGeometry QgsTransectFixedDistanceAlgorithm::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectFixedDistanceAlgorithm::Side orientation, const double angle ) +bool QgsTransectFixedDistanceAlgorithm::prepareAlgorithmTransectParameters( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) { - QgsPoint pLeft; // left point of the line - QgsPoint pRight; // right point of the line - - QgsPolyline line; - - if ( ( orientation == QgsTransectFixedDistanceAlgorithm::Right ) || ( orientation == QgsTransectFixedDistanceAlgorithm::Both ) ) - { - pLeft = point.project( length, angle + 180.0 / M_PI * angleAtVertex ); - if ( orientation != QgsTransectFixedDistanceAlgorithm::Both ) - pRight = point; - } - - if ( ( orientation == QgsTransectFixedDistanceAlgorithm::Left ) || ( orientation == QgsTransectFixedDistanceAlgorithm::Both ) ) - { - pRight = point.project( -length, angle + 180.0 / M_PI * angleAtVertex ); - if ( orientation != QgsTransectFixedDistanceAlgorithm::Both ) - pLeft = point; - } - - line.append( pLeft ); - line.append( pRight ); - - return QgsGeometry::fromPolyline( line ); + mInterval = parameterAsDouble( parameters, QStringLiteral( "INTERVAL" ), context ); + return true; } -///@endcond \ No newline at end of file +std::vector QgsTransectFixedDistanceAlgorithm::generateSamplingPoints( const QgsLineString &line, const QVariantMap &, QgsProcessingContext & ) +{ + std::vector samplingPoints; + + // Sample points at fixed intervals + double totalLength = line.length(); + for ( double d = 0; d <= totalLength; d += mInterval ) + { + QgsPoint *pt = line.interpolatePoint( d ); + samplingPoints.push_back( *pt ); + } + + return samplingPoints; +} + +double QgsTransectFixedDistanceAlgorithm::calculateAzimuth( const QgsLineString &line, const QgsPoint &point, int ) +{ + // For fixed distance sampling, find closest segment + QgsPoint segPt; + QgsVertexId vid; + line.closestSegment( point, segPt, vid, nullptr, Qgis::DEFAULT_SEGMENT_EPSILON ); + QgsVertexId prev( vid.part, vid.ring, vid.vertex - 1 ); + return line.vertexAt( prev ).azimuth( line.vertexAt( vid ) ) * M_PI / 180.0; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmtransectfixeddistance.h b/src/analysis/processing/qgsalgorithmtransectfixeddistance.h index 1c540320381..b4fd8a77c31 100644 --- a/src/analysis/processing/qgsalgorithmtransectfixeddistance.h +++ b/src/analysis/processing/qgsalgorithmtransectfixeddistance.h @@ -1,9 +1,9 @@ /*************************************************************************** qgsalgorithmtransectfixeddistance.h ------------------------------------ - begin : September 2024 - copyright : (C) 2024 by Loïc Bartoletti - email : lbartoletti at tuxfamily dot org + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com ***************************************************************************/ /*************************************************************************** @@ -21,52 +21,34 @@ #define SIP_NO_FILE #include "qgis_sip.h" -#include "qgsprocessingalgorithm.h" +#include "qgsalgorithmtransectbase.h" ///@cond PRIVATE /** * Native transect (fixed distance) algorithm. */ -class QgsTransectFixedDistanceAlgorithm : public QgsProcessingAlgorithm +class QgsTransectFixedDistanceAlgorithm : public QgsTransectAlgorithmBase { public: - /** - * Draw the transect on which side of the line - */ - enum Side - { - Left, - Right, - Both - }; QgsTransectFixedDistanceAlgorithm() = 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; QString shortDescription() const override; - Qgis::ProcessingAlgorithmDocumentationFlags documentationFlags() const override; QgsTransectFixedDistanceAlgorithm *createInstance() const override SIP_FACTORY; protected: - QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + void addAlgorithmParams() override; + bool prepareAlgorithmTransectParameters( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + std::vector generateSamplingPoints( const QgsLineString &line, const QVariantMap ¶meters, QgsProcessingContext &context ) override; + double calculateAzimuth( const QgsLineString &line, const QgsPoint &point, int pointIndex ) override; private: - /** - * Returns the transect of the point \a point with \a length, \a orientation and \a angle. - * \param point The vertex - * \param angleAtVertex Angle at the vertex - * \param length Length of the transect Distance to extend line from input feature - * \param orientation Orientation of the transect - * \param angle Angle of the transect relative to the segment [\a p1 - \a p2] (degrees clockwise) - */ - QgsGeometry calcTransect( const QgsPoint &point, double angleAtVertex, double length, Side orientation, double angle ); + double mInterval = 10.0; }; ///@endcond PRIVATE -#endif // QGSALGORITHMTRANSECTFIXEDDISTANCE_H \ No newline at end of file +#endif // QGSALGORITHMTRANSECTFIXEDDISTANCE_H