QGIS API Documentation 4.1.0-Master (376402f9aeb)
Loading...
Searching...
No Matches
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
20#include "qgsdistancearea.h"
22#include "qgsmultipoint.h"
23#include "qgsvectorlayer.h"
24
25#include <QCollator>
26#include <QString>
27#include <QTextStream>
28
29using namespace Qt::StringLiterals;
30
32
33QString QgsPointsToPathsAlgorithm::name() const
34{
35 return u"pointstopath"_s;
36}
37
38QString QgsPointsToPathsAlgorithm::displayName() const
39{
40 return QObject::tr( "Points to path" );
41}
42
43QString QgsPointsToPathsAlgorithm::shortHelpString() const
44{
45 return QObject::tr(
46 "This algorithm takes a point layer and connects its features to create a new line layer.\n\n"
47 "An attribute or expression may be specified to define the order the points should be connected. "
48 "If no order expression is specified, the feature ID is used.\n\n"
49 "A natural sort can be used when sorting by a string attribute "
50 "or expression (ie. place 'a9' before 'a10').\n\n"
51 "An attribute or expression can be selected to group points having the same value into the same resulting line."
52 );
53}
54
55QString QgsPointsToPathsAlgorithm::shortDescription() const
56{
57 return QObject::tr( "Takes a point layer and connects its features to create a new line layer." );
58}
59
60Qgis::ProcessingAlgorithmDocumentationFlags QgsPointsToPathsAlgorithm::documentationFlags() const
61{
63}
64
65QStringList QgsPointsToPathsAlgorithm::tags() const
66{
67 return QObject::tr( "create,lines,points,connect,convert,join,path" ).split( ',' );
68}
69
70QString QgsPointsToPathsAlgorithm::group() const
71{
72 return QObject::tr( "Vector creation" );
73}
74
75QString QgsPointsToPathsAlgorithm::groupId() const
76{
77 return u"vectorcreation"_s;
78}
79
80void QgsPointsToPathsAlgorithm::initAlgorithm( const QVariantMap & )
81{
82 addParameter( std::make_unique<QgsProcessingParameterFeatureSource>( u"INPUT"_s, QObject::tr( "Input layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorPoint ) ) );
83 addParameter( std::make_unique<QgsProcessingParameterBoolean>( u"CLOSE_PATH"_s, QObject::tr( "Create closed paths" ), false ) );
84 addParameter( std::make_unique<QgsProcessingParameterExpression>( u"ORDER_EXPRESSION"_s, QObject::tr( "Order expression" ), QVariant(), u"INPUT"_s, true ) );
85 addParameter( std::make_unique<QgsProcessingParameterBoolean>( u"NATURAL_SORT"_s, QObject::tr( "Sort text containing numbers naturally" ), false ) );
86 addParameter( std::make_unique<QgsProcessingParameterExpression>( u"GROUP_EXPRESSION"_s, QObject::tr( "Path group expression" ), QVariant(), u"INPUT"_s, true ) );
87 addParameter( std::make_unique<QgsProcessingParameterFeatureSink>( u"OUTPUT"_s, QObject::tr( "Paths" ), Qgis::ProcessingSourceType::VectorLine ) );
88 // TODO QGIS 5: remove parameter. move logic to separate algorithm if needed.
89 addParameter( std::make_unique<QgsProcessingParameterFolderDestination>( u"OUTPUT_TEXT_DIR"_s, QObject::tr( "Directory for text output" ), QVariant(), true, false ) );
90 addOutput( std::make_unique<QgsProcessingOutputNumber>( u"NUM_PATHS"_s, QObject::tr( "Number of paths" ) ) );
91
92 // backwards compatibility parameters
93 // TODO QGIS 5: remove compatibility parameters and their logic
94 auto orderField = std::make_unique<QgsProcessingParameterField>( u"ORDER_FIELD"_s, QObject::tr( "Order field" ), QVariant(), QString(), Qgis::ProcessingFieldParameterDataType::Any, false, true );
95 orderField->setFlags( orderField->flags() | Qgis::ProcessingParameterFlag::Hidden );
96 addParameter( std::move( orderField ) );
97
98 auto groupField = std::make_unique<QgsProcessingParameterField>( u"GROUP_FIELD"_s, QObject::tr( "Group field" ), QVariant(), u"INPUT"_s, Qgis::ProcessingFieldParameterDataType::Any, false, true );
99 groupField->setFlags( groupField->flags() | Qgis::ProcessingParameterFlag::Hidden );
100 addParameter( std::move( groupField ) );
101
102 auto dateFormat = std::make_unique<QgsProcessingParameterString>( u"DATE_FORMAT"_s, QObject::tr( "Date format (if order field is DateTime)" ), QVariant(), false, true );
103 dateFormat->setFlags( dateFormat->flags() | Qgis::ProcessingParameterFlag::Hidden );
104 addParameter( std::move( dateFormat ) );
105}
106
107QgsPointsToPathsAlgorithm *QgsPointsToPathsAlgorithm::createInstance() const
108{
109 return new QgsPointsToPathsAlgorithm();
110}
111
112QVariantMap QgsPointsToPathsAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
113{
114 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, u"INPUT"_s, context ) );
115 if ( !source )
116 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
117
118 const bool closePaths = parameterAsBool( parameters, u"CLOSE_PATH"_s, context );
119
120 QString orderExpressionString = parameterAsString( parameters, u"ORDER_EXPRESSION"_s, context );
121 const QString orderFieldString = parameterAsString( parameters, u"ORDER_FIELD"_s, context );
122 if ( !orderFieldString.isEmpty() )
123 {
124 // this is a backwards compatibility parameter
125 orderExpressionString = QgsExpression::quotedColumnRef( orderFieldString );
126
127 QString dateFormat = parameterAsString( parameters, u"DATE_FORMAT"_s, context );
128 if ( !dateFormat.isEmpty() )
129 {
130 QVector<QPair<QString, QString>> codeMap;
131 codeMap
132 << QPair<QString, QString>( "%%", "%" )
133 << QPair<QString, QString>( "%a", "ddd" )
134 << QPair<QString, QString>( "%A", "dddd" )
135 << QPair<QString, QString>( "%w", "" ) //day of the week 0-6
136 << QPair<QString, QString>( "%d", "dd" )
137 << QPair<QString, QString>( "%b", "MMM" )
138 << QPair<QString, QString>( "%B", "MMMM" )
139 << QPair<QString, QString>( "%m", "MM" )
140 << QPair<QString, QString>( "%y", "yy" )
141 << QPair<QString, QString>( "%Y", "yyyy" )
142 << QPair<QString, QString>( "%H", "hh" )
143 << QPair<QString, QString>( "%I", "hh" ) // 12 hour
144 << QPair<QString, QString>( "%p", "AP" )
145 << QPair<QString, QString>( "%M", "mm" )
146 << QPair<QString, QString>( "%S", "ss" )
147 << QPair<QString, QString>( "%f", "zzz" ) // milliseconds instead of microseconds
148 << QPair<QString, QString>( "%z", "" ) // utc offset
149 << QPair<QString, QString>( "%Z", "" ) // timezone name
150 << QPair<QString, QString>( "%j", "" ) // day of the year
151 << QPair<QString, QString>( "%U", "" ) // week number of the year sunday based
152 << QPair<QString, QString>( "%W", "" ) // week number of the year monday based
153 << QPair<QString, QString>( "%c", "" ) // full datetime
154 << QPair<QString, QString>( "%x", "" ) // full date
155 << QPair<QString, QString>( "%X", "" ) // full time
156 << QPair<QString, QString>( "%G", "yyyy" )
157 << QPair<QString, QString>( "%u", "" ) // day of the week 1-7
158 << QPair<QString, QString>( "%V", "" ); // week number
159 for ( const auto &pair : std::as_const( codeMap ) )
160 {
161 dateFormat.replace( pair.first, pair.second );
162 }
163 orderExpressionString = QString( "to_datetime(%1, '%2')" ).arg( orderExpressionString, dateFormat );
164 }
165 }
166 else if ( orderExpressionString.isEmpty() )
167 {
168 // If no order expression is given, default to the fid
169 orderExpressionString = QString( "$id" );
170 }
171 QgsExpressionContext expressionContext = createExpressionContext( parameters, context, source.get() );
172 QgsExpression orderExpression = QgsExpression( orderExpressionString );
173 if ( orderExpression.hasParserError() )
174 throw QgsProcessingException( orderExpression.parserErrorString() );
175
176 QStringList requiredFields = QStringList( orderExpression.referencedColumns().values() );
177 orderExpression.prepare( &expressionContext );
178
179 QMetaType::Type orderFieldType = QMetaType::Type::QString;
180 if ( orderExpression.isField() )
181 {
182 const QString orderField = qgis::down_cast<const QgsExpressionNodeColumnRef *>( orderExpression.rootNode() )->name();
183 const int orderFieldIndex = source->fields().lookupField( orderField );
184 if ( orderFieldIndex < 0 )
185 {
186 throw QgsProcessingException( QObject::tr( "Order by field %1 does not exist in input layer." ).arg( orderField ) );
187 }
188 orderFieldType = source->fields().field( orderFieldIndex ).type();
189 }
190
191 QString groupExpressionString = parameterAsString( parameters, u"GROUP_EXPRESSION"_s, context );
192 // handle backwards compatibility parameter GROUP_FIELD
193 const QString groupFieldString = parameterAsString( parameters, u"GROUP_FIELD"_s, context );
194 if ( !groupFieldString.isEmpty() )
195 groupExpressionString = QgsExpression::quotedColumnRef( groupFieldString );
196
197 QgsExpression groupExpression = groupExpressionString.isEmpty() ? QgsExpression( QString( "true" ) ) : QgsExpression( groupExpressionString );
198 if ( groupExpression.hasParserError() )
199 throw QgsProcessingException( groupExpression.parserErrorString() );
200
201 QgsFields outputFields = QgsFields();
202 if ( !groupExpressionString.isEmpty() )
203 {
204 requiredFields.append( groupExpression.referencedColumns().values() );
205 if ( groupExpression.isField() )
206 {
207 const QString groupField = qgis::down_cast<const QgsExpressionNodeColumnRef *>( groupExpression.rootNode() )->name();
208 const int groupFieldIndex = source->fields().lookupField( groupField );
209 if ( groupFieldIndex < 0 )
210 {
211 throw QgsProcessingException( QObject::tr( "Group field %1 does not exist in input layer." ).arg( groupField ) );
212 }
213 outputFields.append( source->fields().field( groupFieldIndex ) );
214 }
215 else
216 {
217 outputFields.append( QgsField( u"group"_s, QMetaType::QString ) );
218 }
219 }
220 outputFields.append( QgsField( "begin", orderFieldType ) );
221 outputFields.append( QgsField( "end", orderFieldType ) );
222
223 const bool naturalSort = parameterAsBool( parameters, u"NATURAL_SORT"_s, context );
224 QCollator collator;
225 collator.setNumericMode( true );
226
228 if ( QgsWkbTypes::hasM( source->wkbType() ) )
229 wkbType = QgsWkbTypes::addM( wkbType );
230 if ( QgsWkbTypes::hasZ( source->wkbType() ) )
231 wkbType = QgsWkbTypes::addZ( wkbType );
232
233 QString dest;
234 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, dest, outputFields, wkbType, source->sourceCrs() ) );
235 if ( !sink )
236 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
237
238 const QString textDir = parameterAsString( parameters, u"OUTPUT_TEXT_DIR"_s, context );
239 if ( !textDir.isEmpty() && !QDir().mkpath( textDir ) )
240 {
241 throw QgsProcessingException( QObject::tr( "Failed to create the text output directory" ) );
242 }
243
245 da.setSourceCrs( source->sourceCrs(), context.transformContext() );
246 da.setEllipsoid( context.ellipsoid() );
247
248 // Store the points in a hash with the group identifier as the key
249 QHash<QVariant, QVector<QPair<QVariant, QgsPoint>>> allPoints;
250
251 const QgsFeatureRequest request = QgsFeatureRequest().setSubsetOfAttributes( requiredFields, source->fields() );
253 QgsFeature f;
254 const double totalPoints = source->featureCount() > 0 ? 100.0 / source->featureCount() : 0;
255 long currentPoint = 0;
256 feedback->setProgressText( QObject::tr( "Loading points…" ) );
257 while ( fit.nextFeature( f ) )
258 {
259 if ( feedback->isCanceled() )
260 {
261 break;
262 }
263 feedback->setProgress( 0.5 * currentPoint * totalPoints );
264
265 if ( f.hasGeometry() )
266 {
267 expressionContext.setFeature( f );
268 const QVariant orderValue = orderExpression.evaluate( &expressionContext );
269 const QVariant groupValue = groupExpressionString.isEmpty() ? QVariant() : groupExpression.evaluate( &expressionContext );
270
271 if ( !allPoints.contains( groupValue ) )
272 allPoints[groupValue] = QVector<QPair<QVariant, QgsPoint>>();
273 const QgsAbstractGeometry *geom = f.geometry().constGet();
274 if ( QgsWkbTypes::isMultiType( geom->wkbType() ) )
275 {
276 const QgsMultiPoint mp( *qgsgeometry_cast<const QgsMultiPoint *>( geom ) );
277 for ( auto pit = mp.const_parts_begin(); pit != mp.const_parts_end(); ++pit )
278 {
279 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( *pit ) )
280 {
281 allPoints[groupValue] << qMakePair( orderValue, *point );
282 }
283 }
284 }
285 else
286 {
287 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( geom ) )
288 {
289 allPoints[groupValue] << qMakePair( orderValue, *point );
290 }
291 }
292 }
293 ++currentPoint;
294 }
295
296 int pathCount = 0;
297 currentPoint = 0;
298 QHashIterator<QVariant, QVector<QPair<QVariant, QgsPoint>>> hit( allPoints );
299 feedback->setProgressText( QObject::tr( "Creating paths…" ) );
300 while ( hit.hasNext() )
301 {
302 hit.next();
303 if ( feedback->isCanceled() )
304 {
305 break;
306 }
307 QVector<QPair<QVariant, QgsPoint>> pairs = hit.value();
308
309 if ( naturalSort )
310 {
311 std::stable_sort( pairs.begin(), pairs.end(), [&collator]( const QPair<QVariant, QgsPoint> &pair1, const QPair<QVariant, QgsPoint> &pair2 ) {
312 return collator.compare( pair1.first.toString(), pair2.first.toString() ) < 0;
313 } );
314 }
315 else
316 {
317 std::stable_sort( pairs.begin(), pairs.end(), []( const QPair<QVariant, QgsPoint> &pair1, const QPair<QVariant, QgsPoint> &pair2 ) { return qgsVariantLessThan( pair1.first, pair2.first ); } );
318 }
319
320
321 QVector<QgsPoint> pathPoints;
322 for ( auto pit = pairs.constBegin(); pit != pairs.constEnd(); ++pit )
323 {
324 if ( feedback->isCanceled() )
325 {
326 break;
327 }
328 feedback->setProgress( 50 + 0.5 * currentPoint * totalPoints );
329 pathPoints.append( pit->second );
330 ++currentPoint;
331 }
332 if ( pathPoints.size() < 2 )
333 {
334 feedback->pushInfo( QObject::tr( "Skipping path with group %1 : insufficient vertices" ).arg( hit.key().toString() ) );
335 continue;
336 }
337 if ( closePaths && pathPoints.size() > 2 && pathPoints.constFirst() != pathPoints.constLast() )
338 pathPoints.append( pathPoints.constFirst() );
339
340 QgsFeature outputFeature;
341 QgsAttributes attrs;
342 if ( !groupExpressionString.isEmpty() )
343 attrs.append( hit.key() );
344 attrs.append( pairs.first().first );
345 attrs.append( pairs.last().first );
346 outputFeature.setGeometry( QgsGeometry::fromPolyline( pathPoints ) );
347 outputFeature.setAttributes( attrs );
348 if ( !sink->addFeature( outputFeature, QgsFeatureSink::FastInsert ) )
349 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
350
351 if ( !textDir.isEmpty() )
352 {
353 const QString filename = QDir( textDir ).filePath( hit.key().toString() + QString( ".txt" ) );
354 QFile textFile( filename );
355 if ( !textFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
356 throw QgsProcessingException( QObject::tr( "Cannot open file for writing " ) + filename );
357
358 QTextStream out( &textFile );
359 out << QString(
360 "angle=Azimuth\n"
361 "heading=Coordinate_System\n"
362 "dist_units=Default\n"
363 "startAt=%1;%2;90\n"
364 "survey=Polygonal\n"
365 "[data]\n"
366 )
367 .arg( pathPoints.at( 0 ).x() )
368 .arg( pathPoints.at( 0 ).y() );
369
370 for ( int i = 1; i < pathPoints.size(); ++i )
371 {
372 const double angle = pathPoints.at( i - 1 ).azimuth( pathPoints.at( i ) );
373 double distance = 0;
374 try
375 {
376 distance = da.measureLine( pathPoints.at( i - 1 ), pathPoints.at( i ) );
377 }
378 catch ( QgsCsException & )
379 {
380 throw QgsProcessingException( QObject::tr( "An error occurred while calculating length" ) );
381 }
382 out << QString( "%1;%2;90\n" ).arg( angle ).arg( distance );
383 }
384 }
385
386 ++pathCount;
387 }
388
389 sink->finalize();
390
391 QVariantMap outputs;
392 outputs.insert( u"OUTPUT"_s, dest );
393 outputs.insert( u"NUM_PATHS"_s, pathCount );
394 if ( !textDir.isEmpty() )
395 {
396 outputs.insert( u"OUTPUT_TEXT_DIR"_s, textDir );
397 }
398 return outputs;
399}
400
@ VectorPoint
Vector point layers.
Definition qgis.h:3715
@ VectorLine
Vector line layers.
Definition qgis.h:3716
@ RespectsEllipsoid
Algorithm respects the context's ellipsoid settings, and uses ellipsoidal based measurements.
Definition qgis.h:3803
QFlags< ProcessingAlgorithmDocumentationFlag > ProcessingAlgorithmDocumentationFlags
Flags describing algorithm behavior for documentation purposes.
Definition qgis.h:3812
@ SkipGeometryValidityChecks
Invalid geometry checks should always be skipped. This flag can be useful for algorithms which always...
Definition qgis.h:3895
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:294
@ LineString
LineString.
Definition qgis.h:297
@ Hidden
Parameter is hidden and should not be shown to users.
Definition qgis.h:3948
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).
const QgsExpressionNode * rootNode() const
Returns the root node of the expression.
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:60
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
QgsGeometry geometry
Definition qgsfeature.h:71
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:56
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:65
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:56
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:75
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.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
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 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 Q_INVOKABLE bool hasZ(Qgis::WkbType type)
Tests whether a WKB type contains the z-dimension.
static Q_INVOKABLE bool hasM(Qgis::WkbType type)
Tests whether a WKB type contains m values.
static Q_INVOKABLE bool isMultiType(Qgis::WkbType type)
Returns true if the WKB type is a multi type.
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).
T qgsgeometry_cast(QgsAbstractGeometry *geom)