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