QGIS API Documentation 4.3.0-Master (ffcfc20b9b4)
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 QGS_MARK_ALGORITHM_SOURCE
115
116 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, u"INPUT"_s, context ) );
117 if ( !source )
118 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
119
120 const bool closePaths = parameterAsBool( parameters, u"CLOSE_PATH"_s, context );
121
122 QString orderExpressionString = parameterAsString( parameters, u"ORDER_EXPRESSION"_s, context );
123 const QString orderFieldString = parameterAsString( parameters, u"ORDER_FIELD"_s, context );
124 if ( !orderFieldString.isEmpty() )
125 {
126 // this is a backwards compatibility parameter
127 orderExpressionString = QgsExpression::quotedColumnRef( orderFieldString );
128
129 QString dateFormat = parameterAsString( parameters, u"DATE_FORMAT"_s, context );
130 if ( !dateFormat.isEmpty() )
131 {
132 QVector<QPair<QString, QString>> codeMap;
133 codeMap
134 << QPair<QString, QString>( "%%", "%" )
135 << QPair<QString, QString>( "%a", "ddd" )
136 << QPair<QString, QString>( "%A", "dddd" )
137 << QPair<QString, QString>( "%w", "" ) //day of the week 0-6
138 << QPair<QString, QString>( "%d", "dd" )
139 << QPair<QString, QString>( "%b", "MMM" )
140 << QPair<QString, QString>( "%B", "MMMM" )
141 << QPair<QString, QString>( "%m", "MM" )
142 << QPair<QString, QString>( "%y", "yy" )
143 << QPair<QString, QString>( "%Y", "yyyy" )
144 << QPair<QString, QString>( "%H", "hh" )
145 << QPair<QString, QString>( "%I", "hh" ) // 12 hour
146 << QPair<QString, QString>( "%p", "AP" )
147 << QPair<QString, QString>( "%M", "mm" )
148 << QPair<QString, QString>( "%S", "ss" )
149 << QPair<QString, QString>( "%f", "zzz" ) // milliseconds instead of microseconds
150 << QPair<QString, QString>( "%z", "" ) // utc offset
151 << QPair<QString, QString>( "%Z", "" ) // timezone name
152 << QPair<QString, QString>( "%j", "" ) // day of the year
153 << QPair<QString, QString>( "%U", "" ) // week number of the year sunday based
154 << QPair<QString, QString>( "%W", "" ) // week number of the year monday based
155 << QPair<QString, QString>( "%c", "" ) // full datetime
156 << QPair<QString, QString>( "%x", "" ) // full date
157 << QPair<QString, QString>( "%X", "" ) // full time
158 << QPair<QString, QString>( "%G", "yyyy" )
159 << QPair<QString, QString>( "%u", "" ) // day of the week 1-7
160 << QPair<QString, QString>( "%V", "" ); // week number
161 for ( const auto &pair : std::as_const( codeMap ) )
162 {
163 dateFormat.replace( pair.first, pair.second );
164 }
165 orderExpressionString = QString( "to_datetime(%1, '%2')" ).arg( orderExpressionString, dateFormat );
166 }
167 }
168 else if ( orderExpressionString.isEmpty() )
169 {
170 // If no order expression is given, default to the fid
171 orderExpressionString = QString( "$id" );
172 }
173 QgsExpressionContext expressionContext = createExpressionContext( parameters, context, source.get() );
174 QgsExpression orderExpression = QgsExpression( orderExpressionString );
175 if ( orderExpression.hasParserError() )
176 throw QgsProcessingException( orderExpression.parserErrorString() );
177
178 QStringList requiredFields = QStringList( orderExpression.referencedColumns().values() );
179 orderExpression.prepare( &expressionContext );
180
181 QMetaType::Type orderFieldType = QMetaType::Type::QString;
182 if ( orderExpression.isField() )
183 {
184 const QString orderField = qgis::down_cast<const QgsExpressionNodeColumnRef *>( orderExpression.rootNode() )->name();
185 const int orderFieldIndex = source->fields().lookupField( orderField );
186 if ( orderFieldIndex < 0 )
187 {
188 throw QgsProcessingException( QObject::tr( "Order by field %1 does not exist in input layer." ).arg( orderField ) );
189 }
190 orderFieldType = source->fields().field( orderFieldIndex ).type();
191 }
192
193 QString groupExpressionString = parameterAsString( parameters, u"GROUP_EXPRESSION"_s, context );
194 // handle backwards compatibility parameter GROUP_FIELD
195 const QString groupFieldString = parameterAsString( parameters, u"GROUP_FIELD"_s, context );
196 if ( !groupFieldString.isEmpty() )
197 groupExpressionString = QgsExpression::quotedColumnRef( groupFieldString );
198
199 QgsExpression groupExpression = groupExpressionString.isEmpty() ? QgsExpression( QString( "true" ) ) : QgsExpression( groupExpressionString );
200 if ( groupExpression.hasParserError() )
201 throw QgsProcessingException( groupExpression.parserErrorString() );
202
203 QgsFields outputFields = QgsFields();
204 if ( !groupExpressionString.isEmpty() )
205 {
206 requiredFields.append( groupExpression.referencedColumns().values() );
207 if ( groupExpression.isField() )
208 {
209 const QString groupField = qgis::down_cast<const QgsExpressionNodeColumnRef *>( groupExpression.rootNode() )->name();
210 const int groupFieldIndex = source->fields().lookupField( groupField );
211 if ( groupFieldIndex < 0 )
212 {
213 throw QgsProcessingException( QObject::tr( "Group field %1 does not exist in input layer." ).arg( groupField ) );
214 }
215 outputFields.append( source->fields().field( groupFieldIndex ) );
216 }
217 else
218 {
219 outputFields.append( QgsField( u"group"_s, QMetaType::QString ) );
220 }
221 }
222 outputFields.append( QgsField( "begin", orderFieldType ) );
223 outputFields.append( QgsField( "end", orderFieldType ) );
224
225 const bool naturalSort = parameterAsBool( parameters, u"NATURAL_SORT"_s, context );
226 QCollator collator;
227 collator.setNumericMode( true );
228
230 if ( QgsWkbTypes::hasM( source->wkbType() ) )
231 wkbType = QgsWkbTypes::addM( wkbType );
232 if ( QgsWkbTypes::hasZ( source->wkbType() ) )
233 wkbType = QgsWkbTypes::addZ( wkbType );
234
235 QString dest;
236 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, dest, outputFields, wkbType, source->sourceCrs() ) );
237 if ( !sink )
238 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
239
240 const QString textDir = parameterAsString( parameters, u"OUTPUT_TEXT_DIR"_s, context );
241 if ( !textDir.isEmpty() && !QDir().mkpath( textDir ) )
242 {
243 throw QgsProcessingException( QObject::tr( "Failed to create the text output directory" ) );
244 }
245
247 da.setSourceCrs( source->sourceCrs(), context.transformContext() );
248 da.setEllipsoid( context.ellipsoid() );
249
250 // Store the points in a hash with the group identifier as the key
251 QHash<QVariant, QVector<QPair<QVariant, QgsPoint>>> allPoints;
252
253 const QgsFeatureRequest request = QgsFeatureRequest().setSubsetOfAttributes( requiredFields, source->fields() );
255 QgsFeature f;
256 const double totalPoints = source->featureCount() > 0 ? 100.0 / source->featureCount() : 0;
257 long currentPoint = 0;
258 feedback->setProgressText( QObject::tr( "Loading points…" ) );
259 while ( fit.nextFeature( f ) )
260 {
261 if ( feedback->isCanceled() )
262 {
263 break;
264 }
265 feedback->setProgress( 0.5 * currentPoint * totalPoints );
266
267 if ( f.hasGeometry() )
268 {
269 expressionContext.setFeature( f );
270 const QVariant orderValue = orderExpression.evaluate( &expressionContext );
271 const QVariant groupValue = groupExpressionString.isEmpty() ? QVariant() : groupExpression.evaluate( &expressionContext );
272
273 if ( !allPoints.contains( groupValue ) )
274 allPoints[groupValue] = QVector<QPair<QVariant, QgsPoint>>();
275 const QgsAbstractGeometry *geom = f.geometry().constGet();
276 if ( QgsWkbTypes::isMultiType( geom->wkbType() ) )
277 {
278 const QgsMultiPoint mp( *qgsgeometry_cast<const QgsMultiPoint *>( geom ) );
279 for ( auto pit = mp.const_parts_begin(); pit != mp.const_parts_end(); ++pit )
280 {
281 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( *pit ) )
282 {
283 allPoints[groupValue] << qMakePair( orderValue, *point );
284 }
285 }
286 }
287 else
288 {
289 if ( const QgsPoint *point = qgsgeometry_cast<const QgsPoint *>( geom ) )
290 {
291 allPoints[groupValue] << qMakePair( orderValue, *point );
292 }
293 }
294 }
295 ++currentPoint;
296 }
297
298 int pathCount = 0;
299 currentPoint = 0;
300 QHashIterator<QVariant, QVector<QPair<QVariant, QgsPoint>>> hit( allPoints );
301 feedback->setProgressText( QObject::tr( "Creating paths…" ) );
302 while ( hit.hasNext() )
303 {
304 hit.next();
305 if ( feedback->isCanceled() )
306 {
307 break;
308 }
309 QVector<QPair<QVariant, QgsPoint>> pairs = hit.value();
310
311 if ( naturalSort )
312 {
313 std::stable_sort( pairs.begin(), pairs.end(), [&collator]( const QPair<QVariant, QgsPoint> &pair1, const QPair<QVariant, QgsPoint> &pair2 ) {
314 return collator.compare( pair1.first.toString(), pair2.first.toString() ) < 0;
315 } );
316 }
317 else
318 {
319 std::stable_sort( pairs.begin(), pairs.end(), []( const QPair<QVariant, QgsPoint> &pair1, const QPair<QVariant, QgsPoint> &pair2 ) { return qgsVariantLessThan( pair1.first, pair2.first ); } );
320 }
321
322
323 QVector<QgsPoint> pathPoints;
324 for ( auto pit = pairs.constBegin(); pit != pairs.constEnd(); ++pit )
325 {
326 if ( feedback->isCanceled() )
327 {
328 break;
329 }
330 feedback->setProgress( 50 + 0.5 * currentPoint * totalPoints );
331 pathPoints.append( pit->second );
332 ++currentPoint;
333 }
334 if ( pathPoints.size() < 2 )
335 {
336 feedback->pushInfo( QObject::tr( "Skipping path with group %1 : insufficient vertices" ).arg( hit.key().toString() ) );
337 continue;
338 }
339 if ( closePaths && pathPoints.size() > 2 && pathPoints.constFirst() != pathPoints.constLast() )
340 pathPoints.append( pathPoints.constFirst() );
341
342 QgsFeature outputFeature;
343 QgsAttributes attrs;
344 if ( !groupExpressionString.isEmpty() )
345 attrs.append( hit.key() );
346 attrs.append( pairs.first().first );
347 attrs.append( pairs.last().first );
348 outputFeature.setGeometry( QgsGeometry::fromPolyline( pathPoints ) );
349 outputFeature.setAttributes( attrs );
350 if ( !sink->addFeature( outputFeature, QgsFeatureSink::FastInsert ) )
351 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
352 else
353 feedback->featureAddedToSink( u"OUTPUT"_s );
354
355 if ( !textDir.isEmpty() )
356 {
357 const QString filename = QDir( textDir ).filePath( hit.key().toString() + QString( ".txt" ) );
358 QFile textFile( filename );
359 if ( !textFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
360 throw QgsProcessingException( QObject::tr( "Cannot open file for writing " ) + filename );
361
362 QTextStream out( &textFile );
363 out << QString(
364 "angle=Azimuth\n"
365 "heading=Coordinate_System\n"
366 "dist_units=Default\n"
367 "startAt=%1;%2;90\n"
368 "survey=Polygonal\n"
369 "[data]\n"
370 )
371 .arg( pathPoints.at( 0 ).x() )
372 .arg( pathPoints.at( 0 ).y() );
373
374 for ( int i = 1; i < pathPoints.size(); ++i )
375 {
376 const double angle = pathPoints.at( i - 1 ).azimuth( pathPoints.at( i ) );
377 double distance = 0;
378 try
379 {
380 distance = da.measureLine( pathPoints.at( i - 1 ), pathPoints.at( i ) );
381 }
382 catch ( QgsCsException & )
383 {
384 throw QgsProcessingException( QObject::tr( "An error occurred while calculating length" ) );
385 }
386 out << QString( "%1;%2;90\n" ).arg( angle ).arg( distance );
387 }
388 }
389
390 ++pathCount;
391 }
392
393 sink->finalize();
394 feedback->featureSinkFinalized( u"OUTPUT"_s );
395
396 QVariantMap outputs;
397 outputs.insert( u"OUTPUT"_s, dest );
398 outputs.insert( u"NUM_PATHS"_s, pathCount );
399 if ( !textDir.isEmpty() )
400 {
401 outputs.insert( u"OUTPUT_TEXT_DIR"_s, textDir );
402 }
403 return outputs;
404}
405
@ VectorPoint
Vector point layers.
Definition qgis.h:3750
@ VectorLine
Vector line layers.
Definition qgis.h:3751
@ RespectsEllipsoid
Algorithm respects the context's ellipsoid settings, and uses ellipsoidal based measurements.
Definition qgis.h:3838
QFlags< ProcessingAlgorithmDocumentationFlag > ProcessingAlgorithmDocumentationFlags
Flags describing algorithm behavior for documentation purposes.
Definition qgis.h:3847
@ SkipGeometryValidityChecks
Invalid geometry checks should always be skipped. This flag can be useful for algorithms which always...
Definition qgis.h:3930
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:3983
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:66
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:45
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.
void featureAddedToSink(const QString &output)
Reports that a feature was added to the the sink associated with the specified algorithm output.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
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)