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