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 &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 
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