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