QGIS API Documentation 3.43.0-Master (e01d6d7c4c0)
qgsalgorithmpointstopaths.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmdpointstopaths.cpp
3 ---------------------
4 begin : November 2020
5 copyright : (C) 2020 by Stefanos Natsis
6 email : uclaros at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgsvectorlayer.h"
20#include "qgsmultipoint.h"
21#include "qgsdistancearea.h"
22
23#include <QCollator>
24#include <QTextStream>
25
27
28QString QgsPointsToPathsAlgorithm::name() const
29{
30 return QStringLiteral( "pointstopath" );
31}
32
33QString QgsPointsToPathsAlgorithm::displayName() const
34{
35 return QObject::tr( "Points to path" );
36}
37
38QString QgsPointsToPathsAlgorithm::shortHelpString() const
39{
40 return QObject::tr( "This algorithm takes a point layer and connects its features to create a new line layer.\n\n"
41 "An attribute or expression may be specified to define the order the points should be connected. "
42 "If no order expression is specified, the feature ID is used.\n\n"
43 "A natural sort can be used when sorting by a string attribute "
44 "or expression (ie. place 'a9' before 'a10').\n\n"
45 "An attribute or expression can be selected to group points having the same value into the same resulting line." );
46}
47
48QString QgsPointsToPathsAlgorithm::shortDescription() const
49{
50 return QObject::tr( "Takes a point layer and connects its features to create a new line layer." );
51}
52
53QStringList QgsPointsToPathsAlgorithm::tags() const
54{
55 return QObject::tr( "create,lines,points,connect,convert,join,path" ).split( ',' );
56}
57
58QString QgsPointsToPathsAlgorithm::group() const
59{
60 return QObject::tr( "Vector creation" );
61}
62
63QString QgsPointsToPathsAlgorithm::groupId() const
64{
65 return QStringLiteral( "vectorcreation" );
66}
67
68void QgsPointsToPathsAlgorithm::initAlgorithm( const QVariantMap & )
69{
70 addParameter( std::make_unique<QgsProcessingParameterFeatureSource>( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorPoint ) ) );
71 addParameter( std::make_unique<QgsProcessingParameterBoolean>( QStringLiteral( "CLOSE_PATH" ), QObject::tr( "Create closed paths" ), false ) );
72 addParameter( std::make_unique<QgsProcessingParameterExpression>( QStringLiteral( "ORDER_EXPRESSION" ), QObject::tr( "Order expression" ), QVariant(), QStringLiteral( "INPUT" ), true ) );
73 addParameter( std::make_unique<QgsProcessingParameterBoolean>( QStringLiteral( "NATURAL_SORT" ), QObject::tr( "Sort text containing numbers naturally" ), false ) );
74 addParameter( std::make_unique<QgsProcessingParameterExpression>( QStringLiteral( "GROUP_EXPRESSION" ), QObject::tr( "Path group expression" ), QVariant(), QStringLiteral( "INPUT" ), true ) );
75 addParameter( std::make_unique<QgsProcessingParameterFeatureSink>( QStringLiteral( "OUTPUT" ), QObject::tr( "Paths" ), Qgis::ProcessingSourceType::VectorLine ) );
76 // TODO QGIS 4: remove parameter. move logic to separate algorithm if needed.
77 addParameter( std::make_unique<QgsProcessingParameterFolderDestination>( QStringLiteral( "OUTPUT_TEXT_DIR" ), QObject::tr( "Directory for text output" ), QVariant(), true, false ) );
78 addOutput( std::make_unique<QgsProcessingOutputNumber>( QStringLiteral( "NUM_PATHS" ), QObject::tr( "Number of paths" ) ) );
79
80 // backwards compatibility parameters
81 // TODO QGIS 4: remove compatibility parameters and their logic
82 auto orderField = std::make_unique<QgsProcessingParameterField>( QStringLiteral( "ORDER_FIELD" ), QObject::tr( "Order field" ), QVariant(), QString(), Qgis::ProcessingFieldParameterDataType::Any, false, true );
83 orderField->setFlags( orderField->flags() | Qgis::ProcessingParameterFlag::Hidden );
84 addParameter( std::move( orderField ) );
85
86 auto groupField = std::make_unique<QgsProcessingParameterField>( QStringLiteral( "GROUP_FIELD" ), QObject::tr( "Group field" ), QVariant(), QStringLiteral( "INPUT" ), Qgis::ProcessingFieldParameterDataType::Any, false, true );
87 groupField->setFlags( groupField->flags() | Qgis::ProcessingParameterFlag::Hidden );
88 addParameter( std::move( groupField ) );
89
90 auto dateFormat = std::make_unique<QgsProcessingParameterString>( QStringLiteral( "DATE_FORMAT" ), QObject::tr( "Date format (if order field is DateTime)" ), QVariant(), false, true );
91 dateFormat->setFlags( dateFormat->flags() | Qgis::ProcessingParameterFlag::Hidden );
92 addParameter( std::move( dateFormat ) );
93}
94
95QgsPointsToPathsAlgorithm *QgsPointsToPathsAlgorithm::createInstance() const
96{
97 return new QgsPointsToPathsAlgorithm();
98}
99
100QVariantMap QgsPointsToPathsAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
101{
102 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
103 if ( !source )
104 throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
105
106 const bool closePaths = parameterAsBool( parameters, QStringLiteral( "CLOSE_PATH" ), context );
107
108 QString orderExpressionString = parameterAsString( parameters, QStringLiteral( "ORDER_EXPRESSION" ), context );
109 const QString orderFieldString = parameterAsString( parameters, QStringLiteral( "ORDER_FIELD" ), context );
110 if ( !orderFieldString.isEmpty() )
111 {
112 // this is a backwards compatibility parameter
113 orderExpressionString = QgsExpression::quotedColumnRef( orderFieldString );
114
115 QString dateFormat = parameterAsString( parameters, QStringLiteral( "DATE_FORMAT" ), context );
116 if ( !dateFormat.isEmpty() )
117 {
118 QVector<QPair<QString, QString>> codeMap;
119 codeMap << QPair<QString, QString>( "%%", "%" )
120 << QPair<QString, QString>( "%a", "ddd" )
121 << QPair<QString, QString>( "%A", "dddd" )
122 << QPair<QString, QString>( "%w", "" ) //day of the week 0-6
123 << QPair<QString, QString>( "%d", "dd" )
124 << QPair<QString, QString>( "%b", "MMM" )
125 << QPair<QString, QString>( "%B", "MMMM" )
126 << QPair<QString, QString>( "%m", "MM" )
127 << QPair<QString, QString>( "%y", "yy" )
128 << QPair<QString, QString>( "%Y", "yyyy" )
129 << QPair<QString, QString>( "%H", "hh" )
130 << QPair<QString, QString>( "%I", "hh" ) // 12 hour
131 << QPair<QString, QString>( "%p", "AP" )
132 << QPair<QString, QString>( "%M", "mm" )
133 << QPair<QString, QString>( "%S", "ss" )
134 << QPair<QString, QString>( "%f", "zzz" ) // milliseconds instead of microseconds
135 << QPair<QString, QString>( "%z", "" ) // utc offset
136 << QPair<QString, QString>( "%Z", "" ) // timezone name
137 << QPair<QString, QString>( "%j", "" ) // day of the year
138 << QPair<QString, QString>( "%U", "" ) // week number of the year sunday based
139 << QPair<QString, QString>( "%W", "" ) // week number of the year monday based
140 << QPair<QString, QString>( "%c", "" ) // full datetime
141 << QPair<QString, QString>( "%x", "" ) // full date
142 << QPair<QString, QString>( "%X", "" ) // full time
143 << QPair<QString, QString>( "%G", "yyyy" )
144 << QPair<QString, QString>( "%u", "" ) // day of the week 1-7
145 << QPair<QString, QString>( "%V", "" ); // week number
146 for ( const auto &pair : codeMap )
147 {
148 dateFormat.replace( pair.first, pair.second );
149 }
150 orderExpressionString = QString( "to_datetime(%1, '%2')" ).arg( orderExpressionString ).arg( dateFormat );
151 }
152 }
153 else if ( orderExpressionString.isEmpty() )
154 {
155 // If no order expression is given, default to the fid
156 orderExpressionString = QString( "$id" );
157 }
158 QgsExpressionContext expressionContext = createExpressionContext( parameters, context, source.get() );
159 QgsExpression orderExpression = QgsExpression( orderExpressionString );
160 if ( orderExpression.hasParserError() )
161 throw QgsProcessingException( orderExpression.parserErrorString() );
162
163 QStringList requiredFields = QStringList( orderExpression.referencedColumns().values() );
164 orderExpression.prepare( &expressionContext );
165
166 QMetaType::Type orderFieldType = QMetaType::Type::QString;
167 if ( orderExpression.isField() )
168 {
169 const int orderFieldIndex = source->fields().indexFromName( orderExpression.referencedColumns().values().first() );
170 orderFieldType = source->fields().field( orderFieldIndex ).type();
171 }
172
173
174 QString groupExpressionString = parameterAsString( parameters, QStringLiteral( "GROUP_EXPRESSION" ), context );
175 // handle backwards compatibility parameter GROUP_FIELD
176 const QString groupFieldString = parameterAsString( parameters, QStringLiteral( "GROUP_FIELD" ), context );
177 if ( !groupFieldString.isEmpty() )
178 groupExpressionString = QgsExpression::quotedColumnRef( groupFieldString );
179
180 QgsExpression groupExpression = groupExpressionString.isEmpty() ? QgsExpression( QString( "true" ) ) : QgsExpression( groupExpressionString );
181 if ( groupExpression.hasParserError() )
182 throw QgsProcessingException( groupExpression.parserErrorString() );
183
184 QgsFields outputFields = QgsFields();
185 if ( !groupExpressionString.isEmpty() )
186 {
187 requiredFields.append( groupExpression.referencedColumns().values() );
188 const QgsField field = groupExpression.isField() ? source->fields().field( requiredFields.last() ) : QStringLiteral( "group" );
189 outputFields.append( field );
190 }
191 outputFields.append( QgsField( "begin", orderFieldType ) );
192 outputFields.append( QgsField( "end", orderFieldType ) );
193
194 const bool naturalSort = parameterAsBool( parameters, QStringLiteral( "NATURAL_SORT" ), context );
195 QCollator collator;
196 collator.setNumericMode( true );
197
199 if ( QgsWkbTypes::hasM( source->wkbType() ) )
200 wkbType = QgsWkbTypes::addM( wkbType );
201 if ( QgsWkbTypes::hasZ( source->wkbType() ) )
202 wkbType = QgsWkbTypes::addZ( wkbType );
203
204 QString dest;
205 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, outputFields, wkbType, source->sourceCrs() ) );
206 if ( !sink )
207 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
208
209 const QString textDir = parameterAsString( parameters, QStringLiteral( "OUTPUT_TEXT_DIR" ), context );
210 if ( !textDir.isEmpty() && !QDir().mkpath( textDir ) )
211 {
212 throw QgsProcessingException( QObject::tr( "Failed to create the text output directory" ) );
213 }
214
216 da.setSourceCrs( source->sourceCrs(), context.transformContext() );
217 da.setEllipsoid( context.ellipsoid() );
218
219 // Store the points in a hash with the group identifier as the key
220 QHash<QVariant, QVector<QPair<QVariant, QgsPoint>>> allPoints;
221
222 const QgsFeatureRequest request = QgsFeatureRequest().setSubsetOfAttributes( requiredFields, source->fields() );
224 QgsFeature f;
225 const double totalPoints = source->featureCount() > 0 ? 100.0 / source->featureCount() : 0;
226 long currentPoint = 0;
227 feedback->setProgressText( QObject::tr( "Loading points…" ) );
228 while ( fit.nextFeature( f ) )
229 {
230 if ( feedback->isCanceled() )
231 {
232 break;
233 }
234 feedback->setProgress( 0.5 * currentPoint * totalPoints );
235
236 if ( f.hasGeometry() )
237 {
238 expressionContext.setFeature( f );
239 const QVariant orderValue = orderExpression.evaluate( &expressionContext );
240 const QVariant groupValue = groupExpressionString.isEmpty() ? QVariant() : groupExpression.evaluate( &expressionContext );
241
242 if ( !allPoints.contains( groupValue ) )
243 allPoints[groupValue] = QVector<QPair<QVariant, QgsPoint>>();
244 const QgsAbstractGeometry *geom = f.geometry().constGet();
245 if ( QgsWkbTypes::isMultiType( geom->wkbType() ) )
246 {
247 const QgsMultiPoint mp( *qgsgeometry_cast<const QgsMultiPoint *>( geom ) );
248 for ( auto pit = mp.const_parts_begin(); pit != mp.const_parts_end(); ++pit )
249 {
250 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( *pit ) )
251 {
252 allPoints[groupValue] << qMakePair( orderValue, *point );
253 }
254 }
255 }
256 else
257 {
258 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( geom ) )
259 {
260 allPoints[groupValue] << qMakePair( orderValue, *point );
261 }
262 }
263 }
264 ++currentPoint;
265 }
266
267 int pathCount = 0;
268 currentPoint = 0;
269 QHashIterator<QVariant, QVector<QPair<QVariant, QgsPoint>>> hit( allPoints );
270 feedback->setProgressText( QObject::tr( "Creating paths…" ) );
271 while ( hit.hasNext() )
272 {
273 hit.next();
274 if ( feedback->isCanceled() )
275 {
276 break;
277 }
278 QVector<QPair<QVariant, QgsPoint>> pairs = hit.value();
279
280 if ( naturalSort )
281 {
282 std::stable_sort( pairs.begin(), pairs.end(), [&collator]( const QPair<const QVariant, QgsPoint> &pair1, const QPair<const QVariant, QgsPoint> &pair2 ) {
283 return collator.compare( pair1.first.toString(), pair2.first.toString() ) < 0;
284 } );
285 }
286 else
287 {
288 std::stable_sort( pairs.begin(), pairs.end(), []( const QPair<const QVariant, QgsPoint> &pair1, const QPair<const QVariant, QgsPoint> &pair2 ) {
289 return qgsVariantLessThan( pair1.first, pair2.first );
290 } );
291 }
292
293
294 QVector<QgsPoint> pathPoints;
295 for ( auto pit = pairs.constBegin(); pit != pairs.constEnd(); ++pit )
296 {
297 if ( feedback->isCanceled() )
298 {
299 break;
300 }
301 feedback->setProgress( 50 + 0.5 * currentPoint * totalPoints );
302 pathPoints.append( pit->second );
303 ++currentPoint;
304 }
305 if ( pathPoints.size() < 2 )
306 {
307 feedback->pushInfo( QObject::tr( "Skipping path with group %1 : insufficient vertices" ).arg( hit.key().toString() ) );
308 continue;
309 }
310 if ( closePaths && pathPoints.size() > 2 && pathPoints.constFirst() != pathPoints.constLast() )
311 pathPoints.append( pathPoints.constFirst() );
312
313 QgsFeature outputFeature;
314 QgsAttributes attrs;
315 if ( !groupExpressionString.isEmpty() )
316 attrs.append( hit.key() );
317 attrs.append( pairs.first().first );
318 attrs.append( pairs.last().first );
319 outputFeature.setGeometry( QgsGeometry::fromPolyline( pathPoints ) );
320 outputFeature.setAttributes( attrs );
321 if ( !sink->addFeature( outputFeature, QgsFeatureSink::FastInsert ) )
322 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) );
323
324 if ( !textDir.isEmpty() )
325 {
326 const QString filename = QDir( textDir ).filePath( hit.key().toString() + QString( ".txt" ) );
327 QFile textFile( filename );
328 if ( !textFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
329 throw QgsProcessingException( QObject::tr( "Cannot open file for writing " ) + filename );
330
331 QTextStream out( &textFile );
332#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
333 out.setCodec( "UTF-8" );
334#endif
335 out << QString( "angle=Azimuth\n"
336 "heading=Coordinate_System\n"
337 "dist_units=Default\n"
338 "startAt=%1;%2;90\n"
339 "survey=Polygonal\n"
340 "[data]\n" )
341 .arg( pathPoints.at( 0 ).x() )
342 .arg( pathPoints.at( 0 ).y() );
343
344 for ( int i = 1; i < pathPoints.size(); ++i )
345 {
346 const double angle = pathPoints.at( i - 1 ).azimuth( pathPoints.at( i ) );
347 double distance = 0;
348 try
349 {
350 distance = da.measureLine( pathPoints.at( i - 1 ), pathPoints.at( i ) );
351 }
352 catch ( QgsCsException & )
353 {
354 throw QgsProcessingException( QObject::tr( "An error occurred while calculating length" ) );
355 }
356 out << QString( "%1;%2;90\n" ).arg( angle ).arg( distance );
357 }
358 }
359
360 ++pathCount;
361 }
362
363 sink->finalize();
364
365 QVariantMap outputs;
366 outputs.insert( QStringLiteral( "OUTPUT" ), dest );
367 outputs.insert( QStringLiteral( "NUM_PATHS" ), pathCount );
368 if ( !textDir.isEmpty() )
369 {
370 outputs.insert( QStringLiteral( "OUTPUT_TEXT_DIR" ), textDir );
371 }
372 return outputs;
373}
374
@ VectorPoint
Vector point layers.
@ VectorLine
Vector line layers.
@ SkipGeometryValidityChecks
Invalid geometry checks should always be skipped. This flag can be useful for algorithms which always...
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:256
@ LineString
LineString.
@ Hidden
Parameter is hidden and should not be shown to users.
Abstract base class for all geometries.
A vector of attributes.
Custom exception class for Coordinate Reference System related exceptions.
A general purpose distance and area calculator, capable of performing ellipsoid based calculations.
double measureLine(const QVector< QgsPointXY > &points) const
Measures the length of a line with multiple segments.
void setSourceCrs(const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context)
Sets source spatial reference system crs.
bool setEllipsoid(const QString &ellipsoid)
Sets the ellipsoid by its acronym.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
Handles parsing and evaluation of expressions (formerly called "search strings").
bool prepare(const QgsExpressionContext *context)
Gets the expression ready for evaluation - find out column indexes.
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
QString parserErrorString() const
Returns parser error.
bool isField() const
Checks whether an expression consists only of a single field reference.
QSet< QString > referencedColumns() const
Gets list of columns referenced by the expression.
static QString quotedColumnRef(QString name)
Returns a quoted column reference (in double quotes)
QVariant evaluate()
Evaluate the feature and return the result.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:58
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
QgsGeometry geometry
Definition qgsfeature.h:69
bool hasGeometry() const
Returns true if the feature has an associated geometry.
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:53
Container of fields for a vector layer.
Definition qgsfields.h:46
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
Definition qgsfields.cpp:70
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
static QgsGeometry fromPolyline(const QgsPolyline &polyline)
Creates a new LineString geometry from a list of QgsPoint points.
Multi point geometry collection.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:49
Contains information about the context in which a processing algorithm is executed.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
QString ellipsoid() const
Returns the ellipsoid to use for distance and area calculations.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
virtual void setProgressText(const QString &text)
Sets a progress report text string.
static bool isMultiType(Qgis::WkbType type)
Returns true if the WKB type is a multi type.
static Qgis::WkbType addM(Qgis::WkbType type)
Adds the m dimension to a WKB type and returns the new type.
static Qgis::WkbType addZ(Qgis::WkbType type)
Adds the z dimension to a WKB type and returns the new type.
static bool hasZ(Qgis::WkbType type)
Tests whether a WKB type contains the z-dimension.
static bool hasM(Qgis::WkbType type)
Tests whether a WKB type contains m values.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored)