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