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