QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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 
28 QString QgsPointsToPathsAlgorithm::name() const
29 {
30  return QStringLiteral( "pointstopath" );
31 }
32 
33 QString QgsPointsToPathsAlgorithm::displayName() const
34 {
35  return QObject::tr( "Points to path" );
36 }
37 
38 QString 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 
48 QStringList QgsPointsToPathsAlgorithm::tags() const
49 {
50  return QObject::tr( "create,lines,points,connect,convert,join,path" ).split( ',' );
51 }
52 
53 QString QgsPointsToPathsAlgorithm::group() const
54 {
55  return QObject::tr( "Vector creation" );
56 }
57 
58 QString QgsPointsToPathsAlgorithm::groupId() const
59 {
60  return QStringLiteral( "vectorcreation" );
61 }
62 
63 void QgsPointsToPathsAlgorithm::initAlgorithm( const QVariantMap & )
64 {
65  addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ),
66  QObject::tr( "Input layer" ), QList< int >() << QgsProcessing::TypeVectorPoint ) );
67  addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "CLOSE_PATH" ),
68  QObject::tr( "Create closed paths" ), false, true ) );
69  addParameter( new QgsProcessingParameterExpression( QStringLiteral( "ORDER_EXPRESSION" ),
70  QObject::tr( "Order expression" ), QVariant(), QStringLiteral( "INPUT" ), true ) );
71  addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "NATURAL_SORT" ),
72  QObject::tr( "Sort text containing numbers naturally" ), false, true ) );
73  addParameter( new QgsProcessingParameterExpression( QStringLiteral( "GROUP_EXPRESSION" ),
74  QObject::tr( "Path group expression" ), QVariant(), QStringLiteral( "INPUT" ), true ) );
75  addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ),
76  QObject::tr( "Paths" ), QgsProcessing::TypeVectorLine ) );
77  // TODO QGIS 4: remove parameter. move logic to separate algorithm if needed.
78  addParameter( new QgsProcessingParameterFolderDestination( QStringLiteral( "OUTPUT_TEXT_DIR" ),
79  QObject::tr( "Directory for text output" ), QVariant(), true, false ) );
80  addOutput( new QgsProcessingOutputNumber( QStringLiteral( "NUM_PATHS" ), QObject::tr( "Number of paths" ) ) );
81 
82  // backwards compatibility parameters
83  // TODO QGIS 4: remove compatibility parameters and their logic
84  QgsProcessingParameterField *orderField = new QgsProcessingParameterField( QStringLiteral( "ORDER_FIELD" ),
85  QObject::tr( "Order field" ), QVariant(), QString(), QgsProcessingParameterField::Any, false, true );
86  orderField->setFlags( orderField->flags() | QgsProcessingParameterDefinition::FlagHidden );
87  addParameter( orderField );
88  QgsProcessingParameterField *groupField = new QgsProcessingParameterField( QStringLiteral( "GROUP_FIELD" ),
89  QObject::tr( "Group field" ), QVariant(), QStringLiteral( "INPUT" ), QgsProcessingParameterField::Any, false, true );
90  groupField->setFlags( orderField->flags() | QgsProcessingParameterDefinition::FlagHidden );
91  addParameter( groupField );
92  QgsProcessingParameterString *dateFormat = new QgsProcessingParameterString( QStringLiteral( "DATE_FORMAT" ),
93  QObject::tr( "Date format (if order field is DateTime)" ), QVariant(), false, true );
94  dateFormat->setFlags( orderField->flags() | QgsProcessingParameterDefinition::FlagHidden );
95  addParameter( dateFormat );
96 }
97 
98 QgsPointsToPathsAlgorithm *QgsPointsToPathsAlgorithm::createInstance() const
99 {
100  return new QgsPointsToPathsAlgorithm();
101 }
102 
103 QVariantMap QgsPointsToPathsAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
104 {
105  std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
106  if ( !source )
107  throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
108 
109  const bool closePaths = parameterAsBool( parameters, QStringLiteral( "CLOSE_PATH" ), context );
110 
111  QString orderExpressionString = parameterAsString( parameters, QStringLiteral( "ORDER_EXPRESSION" ), context );
112  const QString orderFieldString = parameterAsString( parameters, QStringLiteral( "ORDER_FIELD" ), context );
113  if ( ! orderFieldString.isEmpty() )
114  {
115  // this is a backwards compatibility parameter
116  orderExpressionString = QgsExpression::quotedColumnRef( orderFieldString );
117 
118  QString dateFormat = parameterAsString( parameters, QStringLiteral( "DATE_FORMAT" ), context );
119  if ( ! dateFormat.isEmpty() )
120  {
121  QVector< QPair< QString, QString > > codeMap;
122  codeMap << QPair< QString, QString >( "%%", "%" )
123  << QPair< QString, QString >( "%a", "ddd" )
124  << QPair< QString, QString >( "%A", "dddd" )
125  << QPair< QString, QString >( "%w", "" ) //day of the week 0-6
126  << QPair< QString, QString >( "%d", "dd" )
127  << QPair< QString, QString >( "%b", "MMM" )
128  << QPair< QString, QString >( "%B", "MMMM" )
129  << QPair< QString, QString >( "%m", "MM" )
130  << QPair< QString, QString >( "%y", "yy" )
131  << QPair< QString, QString >( "%Y", "yyyy" )
132  << QPair< QString, QString >( "%H", "hh" )
133  << QPair< QString, QString >( "%I", "hh" ) // 12 hour
134  << QPair< QString, QString >( "%p", "AP" )
135  << QPair< QString, QString >( "%M", "mm" )
136  << QPair< QString, QString >( "%S", "ss" )
137  << QPair< QString, QString >( "%f", "zzz" ) // milliseconds instead of microseconds
138  << QPair< QString, QString >( "%z", "" ) // utc offset
139  << QPair< QString, QString >( "%Z", "" ) // timezone name
140  << QPair< QString, QString >( "%j", "" ) // day of the year
141  << QPair< QString, QString >( "%U", "" ) // week number of the year sunday based
142  << QPair< QString, QString >( "%W", "" ) // week number of the year monday based
143  << QPair< QString, QString >( "%c", "" ) // full datetime
144  << QPair< QString, QString >( "%x", "" ) // full date
145  << QPair< QString, QString >( "%X", "" ) // full time
146  << QPair< QString, QString >( "%G", "yyyy" )
147  << QPair< QString, QString >( "%u", "" ) // day of the week 1-7
148  << QPair< QString, QString >( "%V", "" ); // week number
149  for ( const auto &pair : codeMap )
150  {
151  dateFormat.replace( pair.first, pair.second );
152  }
153  orderExpressionString = QString( "to_datetime(%1, '%2')" ).arg( orderExpressionString ).arg( dateFormat );
154  }
155  }
156  else if ( orderExpressionString.isEmpty() )
157  {
158  // If no order expression is given, default to the fid
159  orderExpressionString = QString( "$id" );
160  }
161  QgsExpressionContext expressionContext = createExpressionContext( parameters, context, source.get() );
162  QgsExpression orderExpression = QgsExpression( orderExpressionString );
163  if ( orderExpression.hasParserError() )
164  throw QgsProcessingException( orderExpression.parserErrorString() );
165 
166  QStringList requiredFields = QStringList( orderExpression.referencedColumns().values() );
167  orderExpression.prepare( &expressionContext );
168 
169  QVariant::Type orderFieldType = QVariant::String;
170  if ( orderExpression.isField() )
171  {
172  const int orderFieldIndex = source->fields().indexFromName( orderExpression.referencedColumns().values().first() );
173  orderFieldType = source->fields().field( orderFieldIndex ).type();
174  }
175 
176 
177  QString groupExpressionString = parameterAsString( parameters, QStringLiteral( "GROUP_EXPRESSION" ), context );
178  // handle backwards compatibility parameter GROUP_FIELD
179  const QString groupFieldString = parameterAsString( parameters, QStringLiteral( "GROUP_FIELD" ), context );
180  if ( ! groupFieldString.isEmpty() )
181  groupExpressionString = QgsExpression::quotedColumnRef( groupFieldString );
182 
183  QgsExpression groupExpression = groupExpressionString.isEmpty() ? QgsExpression( QString( "true" ) ) : QgsExpression( groupExpressionString );
184  if ( groupExpression.hasParserError() )
185  throw QgsProcessingException( groupExpression.parserErrorString() );
186 
187  QgsFields outputFields = QgsFields();
188  if ( ! groupExpressionString.isEmpty() )
189  {
190  requiredFields.append( groupExpression.referencedColumns().values() );
191  const QgsField field = groupExpression.isField() ? source->fields().field( requiredFields.last() ) : QStringLiteral( "group" );
192  outputFields.append( field );
193  }
194  outputFields.append( QgsField( "begin", orderFieldType ) );
195  outputFields.append( QgsField( "end", orderFieldType ) );
196 
197  const bool naturalSort = parameterAsBool( parameters, QStringLiteral( "NATURAL_SORT" ), context );
198  QCollator collator;
199  collator.setNumericMode( true );
200 
202  if ( QgsWkbTypes::hasM( source->wkbType() ) )
203  wkbType = QgsWkbTypes::addM( wkbType );
204  if ( QgsWkbTypes::hasZ( source->wkbType() ) )
205  wkbType = QgsWkbTypes::addZ( wkbType );
206 
207  QString dest;
208  std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, outputFields, wkbType, source->sourceCrs() ) );
209  if ( !sink )
210  throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
211 
212  const QString textDir = parameterAsString( parameters, QStringLiteral( "OUTPUT_TEXT_DIR" ), context );
213  if ( ! textDir.isEmpty() &&
214  ! QDir( textDir ).exists() )
215  throw QgsProcessingException( QObject::tr( "The text output directory does not exist" ) );
216 
218  da.setSourceCrs( source->sourceCrs(), context.transformContext() );
219  da.setEllipsoid( context.ellipsoid() );
220 
221  // Store the points in a hash with the group identifier as the key
222  QHash< QVariant, QVector< QPair< QVariant, QgsPoint > > > allPoints;
223 
224  const QgsFeatureRequest request = QgsFeatureRequest().setSubsetOfAttributes( requiredFields, source->fields() );
226  QgsFeature f;
227  const double totalPoints = source->featureCount() > 0 ? 100.0 / source->featureCount() : 0;
228  long currentPoint = 0;
229  feedback->setProgressText( QObject::tr( "Loading points…" ) );
230  while ( fit.nextFeature( f ) )
231  {
232  if ( feedback->isCanceled() )
233  {
234  break;
235  }
236  feedback->setProgress( 0.5 * currentPoint * totalPoints );
237 
238  if ( f.hasGeometry() )
239  {
240  expressionContext.setFeature( f );
241  const QVariant orderValue = orderExpression.evaluate( &expressionContext );
242  const QVariant groupValue = groupExpressionString.isEmpty() ? QVariant() : groupExpression.evaluate( &expressionContext );
243 
244  if ( ! allPoints.contains( groupValue ) )
245  allPoints[ groupValue ] = QVector< QPair< QVariant, QgsPoint > >();
246  const QgsAbstractGeometry *geom = f.geometry().constGet();
247  if ( QgsWkbTypes::isMultiType( geom->wkbType() ) )
248  {
249  const QgsMultiPoint mp( *qgsgeometry_cast< const QgsMultiPoint * >( geom ) );
250  for ( auto pit = mp.const_parts_begin(); pit != mp.const_parts_end(); ++pit )
251  {
252  const QgsPoint point( *qgsgeometry_cast< const QgsPoint * >( *pit ) );
253  allPoints[ groupValue ] << qMakePair( orderValue, point );
254  }
255  }
256  else
257  {
258  const QgsPoint point( *qgsgeometry_cast< const QgsPoint * >( geom ) );
259  allPoints[ groupValue ] << qMakePair( orderValue, point );
260  }
261  }
262  ++currentPoint;
263  }
264 
265  int pathCount = 0;
266  currentPoint = 0;
267  QHashIterator< QVariant, QVector< QPair< QVariant, QgsPoint > > > hit( allPoints );
268  feedback->setProgressText( QObject::tr( "Creating paths…" ) );
269  while ( hit.hasNext() )
270  {
271  hit.next();
272  if ( feedback->isCanceled() )
273  {
274  break;
275  }
276  auto pairs = hit.value();
277 
278  if ( naturalSort )
279  {
280  std::stable_sort( pairs.begin(),
281  pairs.end(),
282  [&collator]( const QPair< const QVariant, QgsPoint > &pair1,
283  const QPair< const QVariant, QgsPoint > &pair2 )
284  {
285  return collator.compare( pair1.first.toString(), pair2.first.toString() ) < 0;
286  } );
287  }
288  else
289  {
290  std::stable_sort( pairs.begin(),
291  pairs.end(),
292  []( const QPair< const QVariant, QgsPoint > &pair1,
293  const QPair< const QVariant, QgsPoint > &pair2 )
294  {
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( hit.value().first().first );
324  attrs.append( hit.value().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" ).arg( pathPoints.at( 0 ).x() ).arg( pathPoints.at( 0 ).y() );
347 
348  for ( int i = 1; i < pathPoints.size(); ++i )
349  {
350  const double angle = pathPoints.at( i - 1 ).azimuth( pathPoints.at( i ) );
351  const double distance = da.measureLine( pathPoints.at( i - 1 ), pathPoints.at( i ) );
352  out << QString( "%1;%2;90\n" ).arg( angle ).arg( distance );
353  }
354  }
355 
356  ++pathCount;
357  }
358 
359 
360  QVariantMap outputs;
361  outputs.insert( QStringLiteral( "OUTPUT" ), dest );
362  outputs.insert( QStringLiteral( "NUM_PATHS" ), pathCount );
363  return outputs;
364 }
365 
Abstract base class for all geometries.
A vector of attributes.
Definition: qgsattributes.h:58
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)
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:56
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:153
QgsGeometry geometry
Definition: qgsfeature.h:67
bool hasGeometry() const
Returns true if the feature has an associated geometry.
Definition: qgsfeature.cpp:223
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Definition: qgsfeature.cpp:163
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
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:51
Container of fields for a vector layer.
Definition: qgsfields.h:45
bool append(const QgsField &field, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false)
Definition: qgsfields.cpp:59
const QgsAbstractGeometry * constGet() const SIP_HOLDGIL
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.
Definition: qgsmultipoint.h:30
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.
Definition: qgsexception.h:83
@ FlagSkipGeometryValidityChecks
Invalid geometry checks should always be skipped. This flag can be useful for algorithms which always...
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.
A numeric output for processing algorithms.
A boolean parameter for processing algorithms.
@ FlagHidden
Parameter is hidden and should not be shown to users.
Flags flags() const
Returns any flags associated with the parameter.
void setFlags(Flags flags)
Sets the flags associated with the parameter.
An expression parameter for processing algorithms.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A vector layer or feature source field parameter for processing algorithms.
A folder destination parameter, for specifying the destination path for a folder created by the algor...
A string parameter for processing algorithms.
@ TypeVectorLine
Vector line layers.
Definition: qgsprocessing.h:50
@ TypeVectorPoint
Vector point layers.
Definition: qgsprocessing.h:49
static bool isMultiType(Type type) SIP_HOLDGIL
Returns true if the WKB type is a multi type.
Definition: qgswkbtypes.h:862
static bool hasM(Type type) SIP_HOLDGIL
Tests whether a WKB type contains m values.
Definition: qgswkbtypes.h:1130
Type
The WKB type describes the number of dimensions a geometry has.
Definition: qgswkbtypes.h:70
static Type addZ(Type type) SIP_HOLDGIL
Adds the z dimension to a WKB type and returns the new type.
Definition: qgswkbtypes.h:1176
static bool hasZ(Type type) SIP_HOLDGIL
Tests whether a WKB type contains the z-dimension.
Definition: qgswkbtypes.h:1080
static Type addM(Type type) SIP_HOLDGIL
Adds the m dimension to a WKB type and returns the new type.
Definition: qgswkbtypes.h:1201
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)
Definition: MathUtils.cpp:786
const QgsField & field
Definition: qgsfield.h:463