refactor(processing): add QgsAlgorithmTransectBase used for fixed distance and 'normal' one

This commit is contained in:
Loïc Bartoletti 2025-09-30 09:14:35 +02:00
parent 31a20c0cad
commit c08ce4c652
7 changed files with 393 additions and 427 deletions

View File

@ -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

View File

@ -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<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorLine ) ) );
auto length = std::make_unique<QgsProcessingParameterDistance>( 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<QgsProcessingParameterNumber>( 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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
void QgsTransectAlgorithm::addAlgorithmParams()
{
const Side orientation = static_cast<QgsTransectAlgorithm::Side>( 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<QgsProperty>();
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<QgsProperty>();
if ( orientation == QgsTransectAlgorithm::Both )
length /= 2.0;
std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
if ( !source )
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast<QgsProcessingFeatureSource *>( 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<QgsFeatureSink> 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<const QgsMultiLineString *>( 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<QgsPoint> QgsTransectAlgorithm::generateSamplingPoints( const QgsLineString &line, const QVariantMap &, QgsProcessingContext & )
{
std::vector<QgsPoint> 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

View File

@ -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 &parameters, 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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
std::vector<QgsPoint> generateSamplingPoints( const QgsLineString &line, const QVariantMap &parameters, QgsProcessingContext &context ) override;
double calculateAzimuth( const QgsLineString &line, const QgsPoint &point, int pointIndex ) override;
};
///@endcond PRIVATE

View File

@ -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<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorLine ) ) );
auto length = std::make_unique<QgsProcessingParameterDistance>( 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<QgsProcessingParameterNumber>( 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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
{
mOrientation = static_cast<QgsTransectAlgorithmBase::Side>( 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<QgsProperty>();
mLength = parameterAsDouble( parameters, QStringLiteral( "LENGTH" ), context );
mDynamicLength = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "LENGTH" ) );
if ( mDynamicLength )
mLengthProperty = parameters.value( QStringLiteral( "LENGTH" ) ).value<QgsProperty>();
if ( mOrientation == QgsTransectAlgorithmBase::Both )
mLength /= 2.0;
// Let subclass prepare their specific parameters
if ( !prepareAlgorithmTransectParameters( parameters, context, feedback ) )
return QVariantMap();
std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
if ( !source )
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast<QgsProcessingFeatureSource *>( 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<QgsFeatureSink> 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<const QgsMultiLineString *>( 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<QgsPoint> samplingPoints = generateSamplingPoints( line, parameters, context );
for ( int i = 0; i < static_cast<int>( 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<int>( 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

View File

@ -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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) = 0;
/**
* Processes a line geometry using the specific sampling strategy implemented in subclasses.
*/
QVariantMap processAlgorithm( const QVariantMap &parameters, 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<QgsPoint> generateSamplingPoints( const QgsLineString &line, const QVariantMap &parameters, 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

View File

@ -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<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorLine ) ) );
auto length = std::make_unique<QgsProcessingParameterDistance>( 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<QgsProcessingParameterNumber>( 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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
void QgsTransectFixedDistanceAlgorithm::addAlgorithmParams()
{
const Side orientation = static_cast<QgsTransectFixedDistanceAlgorithm::Side>( 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<QgsProperty>();
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<QgsProperty>();
const double interval = parameterAsDouble( parameters, QStringLiteral( "INTERVAL" ), context );
if ( orientation == QgsTransectFixedDistanceAlgorithm::Both )
length /= 2.0;
std::unique_ptr<QgsFeatureSource> source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
if ( !source )
throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
QgsExpressionContext expressionContext = createExpressionContext( parameters, context, dynamic_cast<QgsProcessingFeatureSource *>( 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<QgsFeatureSink> 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<const QgsMultiLineString *>( inputGeometry.constGet() );
for ( int part = 0; part < multiLine->numGeometries(); ++part )
{
const QgsLineString *lineString = multiLine->lineStringN( part );
if ( !lineString )
continue;
QgsLineString line = *lineString;
std::vector<QgsPoint> 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<int>( 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<int>( 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 &parameters, 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
std::vector<QgsPoint> QgsTransectFixedDistanceAlgorithm::generateSamplingPoints( const QgsLineString &line, const QVariantMap &, QgsProcessingContext & )
{
std::vector<QgsPoint> 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

View File

@ -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 &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
void addAlgorithmParams() override;
bool prepareAlgorithmTransectParameters( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
std::vector<QgsPoint> generateSamplingPoints( const QgsLineString &line, const QVariantMap &parameters, 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
#endif // QGSALGORITHMTRANSECTFIXEDDISTANCE_H