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