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
18 #include "qgsalgorithmpointstopaths.h"
19 #include "qgsvectorlayer.h"
20 #include "qgsmultipoint.h"
21 #include "qgsdistancearea.h"
22
23 #include <QCollator>
24 #include <QTextStream>
25
26 ///@cond PRIVATE
27
name() const28 QString QgsPointsToPathsAlgorithm::name() const
29 {
30 return QStringLiteral( "pointstopath" );
31 }
32
displayName() const33 QString QgsPointsToPathsAlgorithm::displayName() const
34 {
35 return QObject::tr( "Points to path" );
36 }
37
shortHelpString() const38 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
tags() const48 QStringList QgsPointsToPathsAlgorithm::tags() const
49 {
50 return QObject::tr( "create,lines,points,connect,convert,join,path" ).split( ',' );
51 }
52
group() const53 QString QgsPointsToPathsAlgorithm::group() const
54 {
55 return QObject::tr( "Vector creation" );
56 }
57
groupId() const58 QString QgsPointsToPathsAlgorithm::groupId() const
59 {
60 return QStringLiteral( "vectorcreation" );
61 }
62
initAlgorithm(const QVariantMap &)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
createInstance() const98 QgsPointsToPathsAlgorithm *QgsPointsToPathsAlgorithm::createInstance() const
99 {
100 return new QgsPointsToPathsAlgorithm();
101 }
102
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)103 QVariantMap QgsPointsToPathsAlgorithm::processAlgorithm( const QVariantMap ¶meters, 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
201 QgsWkbTypes::Type wkbType = QgsWkbTypes::LineString;
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
217 QgsDistanceArea da = QgsDistanceArea();
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() );
225 QgsFeatureIterator fit = source->getFeatures( request, QgsProcessingFeatureSource::FlagSkipGeometryValidityChecks );
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
366 ///@endcond
367