From 02b0ee02e9d01a0364e60fe12d52e0a761d5bf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Thu, 17 Apr 2025 21:38:14 +0200 Subject: [PATCH 1/4] feat(Transect): Add option to generate a transect at a fixed distance (ignore existing vertices) --- .../processing/qgsalgorithmtransect.cpp | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/analysis/processing/qgsalgorithmtransect.cpp b/src/analysis/processing/qgsalgorithmtransect.cpp index e634fb4ce56..4823a050218 100644 --- a/src/analysis/processing/qgsalgorithmtransect.cpp +++ b/src/analysis/processing/qgsalgorithmtransect.cpp @@ -62,6 +62,11 @@ void QgsTransectAlgorithm::initAlgorithm( const QVariantMap & ) 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 QgsProcessingParameterBoolean( QStringLiteral( "FIXED_DISTANCE" ), QObject::tr( "Use fixed interval sampling (ignore original vertices)" ), false ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Transect" ), Qgis::ProcessingSourceType::VectorLine ) ); } @@ -109,6 +114,11 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter if ( dynamicLength ) lengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value(); + const bool fixedDist = parameterAsBool( parameters, QStringLiteral( "FIXED_DISTANCE" ), context ); + double interval = 0.0; + if ( fixedDist ) + interval = parameterAsDouble( parameters, QStringLiteral( "INTERVAL" ), context ); + if ( orientation == QgsTransectAlgorithm::Both ) length /= 2.0; @@ -145,7 +155,6 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 1; QgsFeature feat; - while ( features.nextFeature( feat ) ) { current++; @@ -174,24 +183,60 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter inputGeometry.convertToMultiType(); const QgsMultiLineString *multiLine = static_cast( inputGeometry.constGet() ); - for ( int id = 0; id < multiLine->numGeometries(); ++id ) + + for ( int part = 0; part < multiLine->numGeometries(); ++part ) { - const QgsLineString *line = multiLine->lineStringN( id ); - QgsAbstractGeometry::vertex_iterator it = line->vertices_begin(); - while ( it != line->vertices_end() ) + const QgsLineString *lineString = multiLine->lineStringN( part ); + if ( !lineString ) + continue; + + QgsLineString line = *lineString; + std::vector samplingPoints; + + // Determine sampling points based on mode (fixed distance or vertices) + if ( fixedDist ) { - const QgsVertexId vertexId = it.vertexId(); - const int i = vertexId.vertex; + double totalLength = line.length(); + for ( double d = 0; d <= totalLength; d += interval ) + { + QgsPoint *pt = line.interpolatePoint( d ); + samplingPoints.push_back( *pt ); + } + } + else + { + for ( auto it = line.vertices_begin(); it != line.vertices_end(); ++it ) + samplingPoints.push_back( *it ); + } + + for ( int i = 0; i < static_cast( samplingPoints.size() ); ++i ) + { + const QgsPoint &pt = samplingPoints[i]; + double azimuth = 0; + + if ( fixedDist ) + { + 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; + } + else + { + azimuth = line.vertexAngle( QgsVertexId( part, 0, i ) ); + } + QgsFeature outFeat; QgsAttributes attrs = feat.attributes(); - attrs << current << number << i + 1 << evaluatedAngle << ( ( orientation == QgsTransectAlgorithm::Both ) ? evaluatedLength * 2 : evaluatedLength ) << orientation; + attrs << current << number << i + 1 << evaluatedAngle + << ( ( orientation == QgsTransectAlgorithm::Both ) ? evaluatedLength * 2 : evaluatedLength ) + << static_cast( orientation ); outFeat.setAttributes( attrs ); - const double angleAtVertex = line->vertexAngle( vertexId ); - outFeat.setGeometry( calcTransect( *it, angleAtVertex, evaluatedLength, orientation, evaluatedAngle ) ); + outFeat.setGeometry( calcTransect( pt, azimuth, evaluatedLength, orientation, evaluatedAngle ) ); if ( !sink->addFeature( outFeat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); number++; - it++; } } } @@ -203,7 +248,6 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter return outputs; } - QgsGeometry QgsTransectAlgorithm::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectAlgorithm::Side orientation, const double angle ) { QgsPoint pLeft; // left point of the line From 37c5b4d6c02fdcae8f6dbe64ce03f425cbde84b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Mon, 29 Sep 2025 18:35:12 +0200 Subject: [PATCH 2/4] feat(processing): add separate transect fixed distance algorithm Split transect functionality into two algorithms per PR feedback - keep original transect algorithm for vertices, add dedicated "Transect (fixed distance)" algorithm for regular interval sampling along lines. --- src/analysis/CMakeLists.txt | 1 + .../processing/qgsalgorithmtransect.cpp | 68 +---- .../qgsalgorithmtransectfixeddistance.cpp | 258 ++++++++++++++++++ .../qgsalgorithmtransectfixeddistance.h | 72 +++++ .../processing/qgsnativealgorithms.cpp | 2 + 5 files changed, 345 insertions(+), 56 deletions(-) create mode 100644 src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp create mode 100644 src/analysis/processing/qgsalgorithmtransectfixeddistance.h diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index a7e63034880..163e01082b1 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/qgsalgorithmtransectfixeddistance.cpp processing/qgsalgorithmtransform.cpp processing/qgsalgorithmtranslate.cpp processing/qgsalgorithmtruncatetable.cpp diff --git a/src/analysis/processing/qgsalgorithmtransect.cpp b/src/analysis/processing/qgsalgorithmtransect.cpp index 4823a050218..e634fb4ce56 100644 --- a/src/analysis/processing/qgsalgorithmtransect.cpp +++ b/src/analysis/processing/qgsalgorithmtransect.cpp @@ -62,11 +62,6 @@ void QgsTransectAlgorithm::initAlgorithm( const QVariantMap & ) 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 QgsProcessingParameterBoolean( QStringLiteral( "FIXED_DISTANCE" ), QObject::tr( "Use fixed interval sampling (ignore original vertices)" ), false ) ); - addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Transect" ), Qgis::ProcessingSourceType::VectorLine ) ); } @@ -114,11 +109,6 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter if ( dynamicLength ) lengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value(); - const bool fixedDist = parameterAsBool( parameters, QStringLiteral( "FIXED_DISTANCE" ), context ); - double interval = 0.0; - if ( fixedDist ) - interval = parameterAsDouble( parameters, QStringLiteral( "INTERVAL" ), context ); - if ( orientation == QgsTransectAlgorithm::Both ) length /= 2.0; @@ -155,6 +145,7 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter const double step = source->featureCount() > 0 ? 100.0 / source->featureCount() : 1; QgsFeature feat; + while ( features.nextFeature( feat ) ) { current++; @@ -183,60 +174,24 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter inputGeometry.convertToMultiType(); const QgsMultiLineString *multiLine = static_cast( inputGeometry.constGet() ); - - for ( int part = 0; part < multiLine->numGeometries(); ++part ) + for ( int id = 0; id < multiLine->numGeometries(); ++id ) { - const QgsLineString *lineString = multiLine->lineStringN( part ); - if ( !lineString ) - continue; - - QgsLineString line = *lineString; - std::vector samplingPoints; - - // Determine sampling points based on mode (fixed distance or vertices) - if ( fixedDist ) + const QgsLineString *line = multiLine->lineStringN( id ); + QgsAbstractGeometry::vertex_iterator it = line->vertices_begin(); + while ( it != line->vertices_end() ) { - double totalLength = line.length(); - for ( double d = 0; d <= totalLength; d += interval ) - { - QgsPoint *pt = line.interpolatePoint( d ); - samplingPoints.push_back( *pt ); - } - } - else - { - for ( auto it = line.vertices_begin(); it != line.vertices_end(); ++it ) - samplingPoints.push_back( *it ); - } - - for ( int i = 0; i < static_cast( samplingPoints.size() ); ++i ) - { - const QgsPoint &pt = samplingPoints[i]; - double azimuth = 0; - - if ( fixedDist ) - { - 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; - } - else - { - azimuth = line.vertexAngle( QgsVertexId( part, 0, i ) ); - } - + 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 ) - << static_cast( orientation ); + attrs << current << number << i + 1 << evaluatedAngle << ( ( orientation == QgsTransectAlgorithm::Both ) ? evaluatedLength * 2 : evaluatedLength ) << orientation; outFeat.setAttributes( attrs ); - outFeat.setGeometry( calcTransect( pt, azimuth, evaluatedLength, orientation, evaluatedAngle ) ); + 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++; } } } @@ -248,6 +203,7 @@ QVariantMap QgsTransectAlgorithm::processAlgorithm( const QVariantMap ¶meter return outputs; } + QgsGeometry QgsTransectAlgorithm::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectAlgorithm::Side orientation, const double angle ) { QgsPoint pLeft; // left point of the line diff --git a/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp b/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp new file mode 100644 index 00000000000..fae813759d9 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmtransectfixeddistance.cpp @@ -0,0 +1,258 @@ +/*************************************************************************** + qgsalgorithmtransectfixeddistance.cpp + ------------------------------------- + begin : September 2024 + copyright : (C) 2024 by Loïc Bartoletti + email : lbartoletti at tuxfamily dot org + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "qgsalgorithmtransectfixeddistance.h" +#include "qgsmultilinestring.h" +#include "qgslinestring.h" + +///@cond PRIVATE + +QString QgsTransectFixedDistanceAlgorithm::name() const +{ + return QStringLiteral( "transectfixeddistance" ); +} + +QString QgsTransectFixedDistanceAlgorithm::displayName() const +{ + return QObject::tr( "Transect (fixed distance)" ); +} + +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" ) + + QObject::tr( "A transect is a line oriented from an angle (by default perpendicular) to the input polylines at regular intervals." ) + + QStringLiteral( "\n\n" ) + + QObject::tr( "Field(s) from feature(s) are returned in the transect with these new fields:\n" ) + + QObject::tr( "- TR_FID: ID of the original feature\n" ) + + QObject::tr( "- TR_ID: ID of the transect. Each transect have an unique ID\n" ) + + QObject::tr( "- TR_SEGMENT: ID of the segment of the linestring\n" ) + + QObject::tr( "- TR_ANGLE: Angle in degrees from the original line at the vertex\n" ) + + QObject::tr( "- TR_LENGTH: Total length of the transect returned\n" ) + + QObject::tr( "- TR_ORIENT: Side of the transect (only on the left or right of the line, or both side)\n" ); +} + +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 ) +{ + 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; +} + +QgsGeometry QgsTransectFixedDistanceAlgorithm::calcTransect( const QgsPoint &point, const double angleAtVertex, const double length, const QgsTransectFixedDistanceAlgorithm::Side orientation, const double angle ) +{ + 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 ); +} + +///@endcond \ No newline at end of file diff --git a/src/analysis/processing/qgsalgorithmtransectfixeddistance.h b/src/analysis/processing/qgsalgorithmtransectfixeddistance.h new file mode 100644 index 00000000000..1c540320381 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmtransectfixeddistance.h @@ -0,0 +1,72 @@ +/*************************************************************************** + qgsalgorithmtransectfixeddistance.h + ------------------------------------ + begin : September 2024 + copyright : (C) 2024 by Loïc Bartoletti + email : lbartoletti at tuxfamily dot org + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 QGSALGORITHMTRANSECTFIXEDDISTANCE_H +#define QGSALGORITHMTRANSECTFIXEDDISTANCE_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native transect (fixed distance) algorithm. + */ +class QgsTransectFixedDistanceAlgorithm : public QgsProcessingAlgorithm +{ + 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; + + 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 ); +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMTRANSECTFIXEDDISTANCE_H \ No newline at end of file diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 96ccc4c7fde..277e978748f 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -271,6 +271,7 @@ #include "qgsalgorithmtaperedbuffer.h" #include "qgsalgorithmtinmeshcreation.h" #include "qgsalgorithmtransect.h" +#include "qgsalgorithmtransectfixeddistance.h" #include "qgsalgorithmtransform.h" #include "qgsalgorithmtranslate.h" #include "qgsalgorithmtruncatetable.h" @@ -636,6 +637,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsTaperedBufferAlgorithm() ); addAlgorithm( new QgsTinMeshCreationAlgorithm() ); addAlgorithm( new QgsTransectAlgorithm() ); + addAlgorithm( new QgsTransectFixedDistanceAlgorithm() ); addAlgorithm( new QgsTransferAnnotationsFromMainAlgorithm() ); addAlgorithm( new QgsTransformAlgorithm() ); addAlgorithm( new QgsTranslateAlgorithm() ); From 31a20c0cad94232be3e75d32aad6522648c295d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Tue, 30 Sep 2025 07:50:08 +0200 Subject: [PATCH 3/4] test(transect): Add a test for transect with fixed distance --- .../expected/transect_fixed_single_both.gml | 790 ++++++++++++++++++ .../expected/transect_fixed_single_both.xsd | 97 +++ .../tests/testdata/qgis_algorithm_tests3.yaml | 15 + 3 files changed, 902 insertions(+) create mode 100644 python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.gml create mode 100644 python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.xsd diff --git a/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.gml b/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.gml new file mode 100644 index 00000000000..920893acc51 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.gml @@ -0,0 +1,790 @@ + + + -1007699.67743807 1334639.79376903-1007695.38013693 1334643.87803237 + + + + -1007699.39626005 1334640.42219045-1007698.51286553 1334640.89082049 + -1007699.39626005 1334640.89082049 -1007698.51286553 1334640.42219045 + 1 + 0 + 0 + 1 + 90.00 + 1.000000 + 2 + + + + + -1007699.48998606 1334640.24551155-1007698.60659153 1334640.71414158 + -1007699.48998606 1334640.71414158 -1007698.60659153 1334640.24551155 + 1 + 0 + 1 + 2 + 90.00 + 1.000000 + 2 + + + + + -1007699.58371207 1334640.06883264-1007698.70031754 1334640.53746267 + -1007699.58371207 1334640.53746267 -1007698.70031754 1334640.06883264 + 1 + 0 + 2 + 3 + 90.00 + 1.000000 + 2 + + + + + -1007699.67743807 1334639.89215374-1007698.79404354 1334640.36078377 + -1007699.67743807 1334640.36078377 -1007698.79404354 1334639.89215374 + 1 + 0 + 3 + 4 + 90.00 + 1.000000 + 2 + + + + + -1007698.87625616 1334641.0871433-1007697.8764214 1334641.10532212 + -1007698.87625616 1334641.10532212 -1007697.8764214 1334641.0871433 + 2 + 1 + 4 + 1 + 90.00 + 1.000000 + 2 + + + + + -1007698.87989192 1334640.88717635-1007697.88005717 1334640.90535517 + -1007698.87989192 1334640.90535517 -1007697.88005717 1334640.88717635 + 2 + 1 + 5 + 2 + 90.00 + 1.000000 + 2 + + + + + -1007698.88352768 1334640.6872094-1007697.88369293 1334640.70538822 + -1007698.88352768 1334640.70538822 -1007697.88369293 1334640.6872094 + 2 + 1 + 6 + 3 + 90.00 + 1.000000 + 2 + + + + + -1007698.57444492 1334640.06062766-1007698.14727692 1334640.96479993 + -1007698.57444492 1334640.06062766 -1007698.14727692 1334640.96479993 + 2 + 1 + 7 + 4 + 90.00 + 1.000000 + 2 + + + + + -1007698.39361047 1334639.97519406-1007697.96644246 1334640.87936633 + -1007698.39361047 1334639.97519406 -1007697.96644246 1334640.87936633 + 2 + 1 + 8 + 5 + 90.00 + 1.000000 + 2 + + + + + -1007698.21277601 1334639.88976046-1007697.78560801 1334640.79393273 + -1007698.21277601 1334639.88976046 -1007697.78560801 1334640.79393273 + 2 + 1 + 9 + 6 + 90.00 + 1.000000 + 2 + + + + + -1007697.43018498 1334641.18324138-1007696.5063014 1334641.56591506 + -1007697.43018498 1334641.56591506 -1007696.5063014 1334641.18324138 + 3 + 2 + 10 + 1 + 90.00 + 1.000000 + 2 + + + + + -1007697.50671971 1334640.99846467-1007696.58283614 1334641.38113835 + -1007697.50671971 1334641.38113835 -1007696.58283614 1334640.99846467 + 3 + 2 + 11 + 2 + 90.00 + 1.000000 + 2 + + + + + -1007697.58325445 1334640.81368795-1007696.65937088 1334641.19636163 + -1007697.58325445 1334641.19636163 -1007696.65937088 1334640.81368795 + 3 + 2 + 12 + 3 + 90.00 + 1.000000 + 2 + + + + + -1007697.65978918 1334640.62891124-1007696.73590561 1334641.01158492 + -1007697.65978918 1334641.01158492 -1007696.73590561 1334640.62891124 + 3 + 2 + 13 + 4 + 90.00 + 1.000000 + 2 + + + + + -1007697.73672317 1334640.44900368-1007696.80824647 1334640.82039432 + -1007697.73672317 1334640.82039432 -1007696.80824647 1334640.44900368 + 3 + 2 + 14 + 5 + 90.00 + 1.000000 + 2 + + + + + -1007697.81121219 1334640.26596432-1007696.88031755 1334640.63125212 + -1007697.81121219 1334640.63125212 -1007696.88031755 1334640.26596432 + 3 + 2 + 15 + 6 + 90.00 + 1.000000 + 2 + + + + + -1007697.88853468 1334640.09745328-1007696.94386101 1334640.42546496 + -1007697.88853468 1334640.42546496 -1007696.94386101 1334640.09745328 + 3 + 2 + 16 + 7 + 90.00 + 1.000000 + 2 + + + + + -1007697.97399382 1334639.99112039-1007696.987237 1334640.15332697 + -1007697.97399382 1334640.15332697 -1007696.987237 1334639.99112039 + 3 + 2 + 17 + 8 + 90.00 + 1.000000 + 2 + + + + + -1007698.00643513 1334639.79376903-1007697.01967831 1334639.9559756 + -1007698.00643513 1334639.9559756 -1007697.01967831 1334639.79376903 + 3 + 2 + 18 + 9 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334641.37803237-1007697.38013693 1334641.37803237 + -1007697.38013693 1334641.37803237 -1007698.38013693 1334641.37803237 + 4 + 3 + 19 + 1 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334641.57803237-1007697.38013693 1334641.57803237 + -1007697.38013693 1334641.57803237 -1007698.38013693 1334641.57803237 + 4 + 3 + 20 + 2 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334641.77803237-1007697.38013693 1334641.77803237 + -1007697.38013693 1334641.77803237 -1007698.38013693 1334641.77803237 + 4 + 3 + 21 + 3 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334641.97803237-1007697.38013693 1334641.97803237 + -1007697.38013693 1334641.97803237 -1007698.38013693 1334641.97803237 + 4 + 3 + 22 + 4 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334642.17803237-1007697.38013693 1334642.17803237 + -1007697.38013693 1334642.17803237 -1007698.38013693 1334642.17803237 + 4 + 3 + 23 + 5 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334642.37803237-1007697.38013693 1334642.37803237 + -1007697.38013693 1334642.37803237 -1007698.38013693 1334642.37803237 + 4 + 3 + 24 + 6 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334642.57803237-1007697.38013693 1334642.57803237 + -1007697.38013693 1334642.57803237 -1007698.38013693 1334642.57803237 + 4 + 3 + 25 + 7 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334642.77803237-1007697.38013693 1334642.77803237 + -1007697.38013693 1334642.77803237 -1007698.38013693 1334642.77803237 + 4 + 3 + 26 + 8 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334642.97803237-1007697.38013693 1334642.97803237 + -1007697.38013693 1334642.97803237 -1007698.38013693 1334642.97803237 + 4 + 3 + 27 + 9 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334643.17803237-1007697.38013693 1334643.17803237 + -1007697.38013693 1334643.17803237 -1007698.38013693 1334643.17803237 + 4 + 3 + 28 + 10 + 90.00 + 1.000000 + 2 + + + + + -1007698.38013693 1334643.37803237-1007697.38013693 1334643.37803237 + -1007697.38013693 1334643.37803237 -1007698.38013693 1334643.37803237 + 4 + 3 + 29 + 11 + 90.00 + 1.000000 + 2 + + + + + -1007697.68013693 1334642.87803237-1007697.68013693 1334643.87803237 + -1007697.68013693 1334642.87803237 -1007697.68013693 1334643.87803237 + 4 + 3 + 30 + 12 + 90.00 + 1.000000 + 2 + + + + + -1007697.48013693 1334642.87803237-1007697.48013693 1334643.87803237 + -1007697.48013693 1334642.87803237 -1007697.48013693 1334643.87803237 + 4 + 3 + 31 + 13 + 90.00 + 1.000000 + 2 + + + + + -1007697.28013693 1334642.87803237-1007697.28013693 1334643.87803237 + -1007697.28013693 1334642.87803237 -1007697.28013693 1334643.87803237 + 4 + 3 + 32 + 14 + 90.00 + 1.000000 + 2 + + + + + -1007697.08013693 1334642.87803237-1007697.08013693 1334643.87803237 + -1007697.08013693 1334642.87803237 -1007697.08013693 1334643.87803237 + 4 + 3 + 33 + 15 + 90.00 + 1.000000 + 2 + + + + + -1007696.88013693 1334642.87803237-1007696.88013693 1334643.87803237 + -1007696.88013693 1334642.87803237 -1007696.88013693 1334643.87803237 + 4 + 3 + 34 + 16 + 90.00 + 1.000000 + 2 + + + + + -1007696.68013693 1334642.87803237-1007696.68013693 1334643.87803237 + -1007696.68013693 1334642.87803237 -1007696.68013693 1334643.87803237 + 4 + 3 + 35 + 17 + 90.00 + 1.000000 + 2 + + + + + -1007696.48013693 1334642.87803237-1007696.48013693 1334643.87803237 + -1007696.48013693 1334642.87803237 -1007696.48013693 1334643.87803237 + 4 + 3 + 36 + 18 + 90.00 + 1.000000 + 2 + + + + + -1007696.28013693 1334642.87803237-1007696.28013693 1334643.87803237 + -1007696.28013693 1334642.87803237 -1007696.28013693 1334643.87803237 + 4 + 3 + 37 + 19 + 90.00 + 1.000000 + 2 + + + + + -1007696.08013693 1334642.87803237-1007696.08013693 1334643.87803237 + -1007696.08013693 1334642.87803237 -1007696.08013693 1334643.87803237 + 4 + 3 + 38 + 20 + 90.00 + 1.000000 + 2 + + + + + -1007695.88013693 1334642.87803237-1007695.88013693 1334643.87803237 + -1007695.88013693 1334642.87803237 -1007695.88013693 1334643.87803237 + 4 + 3 + 39 + 21 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334643.17803237-1007695.38013693 1334643.17803237 + -1007696.38013693 1334643.17803237 -1007695.38013693 1334643.17803237 + 4 + 3 + 40 + 22 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334642.97803237-1007695.38013693 1334642.97803237 + -1007696.38013693 1334642.97803237 -1007695.38013693 1334642.97803237 + 4 + 3 + 41 + 23 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334642.77803237-1007695.38013693 1334642.77803237 + -1007696.38013693 1334642.77803237 -1007695.38013693 1334642.77803237 + 4 + 3 + 42 + 24 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334642.57803237-1007695.38013693 1334642.57803237 + -1007696.38013693 1334642.57803237 -1007695.38013693 1334642.57803237 + 4 + 3 + 43 + 25 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334642.37803237-1007695.38013693 1334642.37803237 + -1007696.38013693 1334642.37803237 -1007695.38013693 1334642.37803237 + 4 + 3 + 44 + 26 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334642.17803237-1007695.38013693 1334642.17803237 + -1007696.38013693 1334642.17803237 -1007695.38013693 1334642.17803237 + 4 + 3 + 45 + 27 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334641.97803237-1007695.38013693 1334641.97803237 + -1007696.38013693 1334641.97803237 -1007695.38013693 1334641.97803237 + 4 + 3 + 46 + 28 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334641.77803237-1007695.38013693 1334641.77803237 + -1007696.38013693 1334641.77803237 -1007695.38013693 1334641.77803237 + 4 + 3 + 47 + 29 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334641.57803237-1007695.38013693 1334641.57803237 + -1007696.38013693 1334641.57803237 -1007695.38013693 1334641.57803237 + 4 + 3 + 48 + 30 + 90.00 + 1.000000 + 2 + + + + + -1007696.38013693 1334641.37803237-1007695.38013693 1334641.37803237 + -1007696.38013693 1334641.37803237 -1007695.38013693 1334641.37803237 + 4 + 3 + 49 + 31 + 90.00 + 1.000000 + 2 + + + + + -1007696.08172314 1334640.8774-1007696.0785487 1334641.87739496 + -1007696.08172314 1334641.87739496 -1007696.0785487 1334640.8774 + 4 + 3 + 50 + 32 + 90.00 + 1.000000 + 2 + + + + + -1007696.28172214 1334640.87676511-1007696.27854769 1334641.87676007 + -1007696.28172214 1334641.87676007 -1007696.27854769 1334640.87676511 + 4 + 3 + 51 + 33 + 90.00 + 1.000000 + 2 + + + + + -1007696.48172113 1334640.87613022-1007696.47854668 1334641.87612518 + -1007696.48172113 1334641.87612518 -1007696.47854668 1334640.87613022 + 4 + 3 + 52 + 34 + 90.00 + 1.000000 + 2 + + + + + -1007696.68172012 1334640.87549533-1007696.67854568 1334641.8754903 + -1007696.68172012 1334641.8754903 -1007696.67854568 1334640.87549533 + 4 + 3 + 53 + 35 + 90.00 + 1.000000 + 2 + + + + + -1007696.88171911 1334640.87486045-1007696.87854467 1334641.87485541 + -1007696.88171911 1334641.87485541 -1007696.87854467 1334640.87486045 + 4 + 3 + 54 + 36 + 90.00 + 1.000000 + 2 + + + + + -1007697.08202457 1334640.87500562-1007697.07823672 1334641.87499845 + -1007697.07823672 1334641.87499845 -1007697.08202457 1334640.87500562 + 4 + 3 + 55 + 37 + 90.00 + 1.000000 + 2 + + + + + -1007697.28202314 1334640.8757632-1007697.27823528 1334641.87575602 + -1007697.27823528 1334641.87575602 -1007697.28202314 1334640.8757632 + 4 + 3 + 56 + 38 + 90.00 + 1.000000 + 2 + + + + + -1007697.4820217 1334640.87652077-1007697.47823385 1334641.87651359 + -1007697.47823385 1334641.87651359 -1007697.4820217 1334640.87652077 + 4 + 3 + 57 + 39 + 90.00 + 1.000000 + 2 + + + + + -1007697.68202027 1334640.87727834-1007697.67823241 1334641.87727117 + -1007697.67823241 1334641.87727117 -1007697.68202027 1334640.87727834 + 4 + 3 + 58 + 40 + 90.00 + 1.000000 + 2 + + + + + -1007698.38012491 1334641.37803232-1007697.38012491 1334641.37803232 + -1007697.38012491 1334641.37803232 -1007698.38012491 1334641.37803232 + 4 + 3 + 59 + 41 + 90.00 + 1.000000 + 2 + + + diff --git a/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.xsd b/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.xsd new file mode 100644 index 00000000000..ce4c73ecd5a --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/transect_fixed_single_both.xsd @@ -0,0 +1,97 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml index b73e5b0c7e0..26fe780b740 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml @@ -2466,6 +2466,21 @@ tests: name: expected/transect_multi_both_2_30.gml type: vector + - algorithm: native:transectfixeddistance + name: Test (native:transectfixeddistance) + params: + ANGLE: 90.0 + INPUT: + name: custom/transect_single.gml + type: vector + INTERVAL: 0.20 + LENGTH: 1.0 + SIDE: 2 + results: + OUTPUT: + name: expected/transect_fixed_single_both.gml + type: vector + - algorithm: qgis:distancematrix name: Linear (N*k x 3) distance matrix params: From c08ce4c652e110d957e8374929f27d7fd40b468f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Tue, 30 Sep 2025 09:14:35 +0200 Subject: [PATCH 4/4] refactor(processing): add QgsAlgorithmTransectBase used for fixed distance and 'normal' one --- src/analysis/CMakeLists.txt | 1 + .../processing/qgsalgorithmtransect.cpp | 199 ++------------- .../processing/qgsalgorithmtransect.h | 35 +-- .../processing/qgsalgorithmtransectbase.cpp | 221 +++++++++++++++++ .../processing/qgsalgorithmtransectbase.h | 97 ++++++++ .../qgsalgorithmtransectfixeddistance.cpp | 227 +++--------------- .../qgsalgorithmtransectfixeddistance.h | 40 +-- 7 files changed, 393 insertions(+), 427 deletions(-) create mode 100644 src/analysis/processing/qgsalgorithmtransectbase.cpp create mode 100644 src/analysis/processing/qgsalgorithmtransectbase.h 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