Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsalgorithmjoinbynearest.cpp
3 : : ---------------------
4 : : begin : April 2017
5 : : copyright : (C) 2017 by Nyall Dawson
6 : : email : nyall dot dawson 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 "qgsalgorithmjoinbynearest.h"
19 : : #include "qgsprocessingoutputs.h"
20 : : #include "qgslinestring.h"
21 : :
22 : : #include <algorithm>
23 : :
24 : : ///@cond PRIVATE
25 : :
26 : 0 : QString QgsJoinByNearestAlgorithm::name() const
27 : : {
28 : 0 : return QStringLiteral( "joinbynearest" );
29 : : }
30 : :
31 : 0 : QString QgsJoinByNearestAlgorithm::displayName() const
32 : : {
33 : 0 : return QObject::tr( "Join attributes by nearest" );
34 : : }
35 : :
36 : 0 : QStringList QgsJoinByNearestAlgorithm::tags() const
37 : : {
38 : 0 : return QObject::tr( "join,connect,attributes,values,fields,tables,proximity,closest,neighbour,neighbor,n-nearest,distance" ).split( ',' );
39 : 0 : }
40 : :
41 : 0 : QString QgsJoinByNearestAlgorithm::group() const
42 : : {
43 : 0 : return QObject::tr( "Vector general" );
44 : : }
45 : :
46 : 0 : QString QgsJoinByNearestAlgorithm::groupId() const
47 : : {
48 : 0 : return QStringLiteral( "vectorgeneral" );
49 : : }
50 : :
51 : 0 : void QgsJoinByNearestAlgorithm::initAlgorithm( const QVariantMap & )
52 : : {
53 : 0 : addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ),
54 : 0 : QObject::tr( "Input layer" ) ) );
55 : 0 : addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT_2" ),
56 : 0 : QObject::tr( "Input layer 2" ) ) );
57 : :
58 : 0 : addParameter( new QgsProcessingParameterField( QStringLiteral( "FIELDS_TO_COPY" ),
59 : 0 : QObject::tr( "Layer 2 fields to copy (leave empty to copy all fields)" ),
60 : 0 : QVariant(), QStringLiteral( "INPUT_2" ), QgsProcessingParameterField::Any,
61 : : true, true ) );
62 : :
63 : 0 : addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "DISCARD_NONMATCHING" ),
64 : 0 : QObject::tr( "Discard records which could not be joined" ),
65 : 0 : false ) );
66 : :
67 : 0 : addParameter( new QgsProcessingParameterString( QStringLiteral( "PREFIX" ),
68 : 0 : QObject::tr( "Joined field prefix" ), QVariant(), false, true ) );
69 : :
70 : 0 : addParameter( new QgsProcessingParameterNumber( QStringLiteral( "NEIGHBORS" ),
71 : 0 : QObject::tr( "Maximum nearest neighbors" ), QgsProcessingParameterNumber::Integer, 1, false, 1 ) );
72 : :
73 : 0 : addParameter( new QgsProcessingParameterDistance( QStringLiteral( "MAX_DISTANCE" ),
74 : 0 : QObject::tr( "Maximum distance" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ) );
75 : :
76 : 0 : addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Joined layer" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, true ) );
77 : :
78 : 0 : std::unique_ptr< QgsProcessingParameterFeatureSink > nonMatchingSink = std::make_unique< QgsProcessingParameterFeatureSink >(
79 : 0 : QStringLiteral( "NON_MATCHING" ), QObject::tr( "Unjoinable features from first layer" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, false );
80 : : // TODO GUI doesn't support advanced outputs yet
81 : : //nonMatchingSink->setFlags(nonMatchingSink->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
82 : 0 : addParameter( nonMatchingSink.release() );
83 : :
84 : 0 : addOutput( new QgsProcessingOutputNumber( QStringLiteral( "JOINED_COUNT" ), QObject::tr( "Number of joined features from input table" ) ) );
85 : 0 : addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNJOINABLE_COUNT" ), QObject::tr( "Number of unjoinable features from input table" ) ) );
86 : 0 : }
87 : :
88 : 0 : QString QgsJoinByNearestAlgorithm::shortHelpString() const
89 : : {
90 : 0 : return QObject::tr( "This algorithm takes an input vector layer and creates a new vector layer that is an extended version of the "
91 : : "input one, with additional attributes in its attribute table.\n\n"
92 : : "The additional attributes and their values are taken from a second vector layer, where features are joined "
93 : : "by finding the closest features from each layer. By default only the single nearest feature is joined,"
94 : : "but optionally the join can use the n-nearest neighboring features instead. If multiple features are found "
95 : : "with identical distances these will all be returned (even if the total number of features exceeds the specified "
96 : : "maximum feature count).\n\n"
97 : : "If a maximum distance is specified, then only features which are closer than this distance "
98 : : "will be matched.\n\n"
99 : : "The output features will contain the selected attributes from the nearest feature, "
100 : : "along with new attributes for the distance to the near feature, the index of the feature, "
101 : : "and the coordinates of the closest point on the input feature (feature_x, feature_y) "
102 : : "to the matched nearest feature, and the coordinates of the closet point on the matched feature "
103 : : "(nearest_x, nearest_y).\n\n"
104 : : "This algorithm uses purely Cartesian calculations for distance, and does not consider "
105 : : "geodetic or ellipsoid properties when determining feature proximity." );
106 : : }
107 : :
108 : 0 : QString QgsJoinByNearestAlgorithm::shortDescription() const
109 : : {
110 : 0 : return QObject::tr( "Joins a layer to another layer, using the closest features (nearest neighbors)." );
111 : : }
112 : :
113 : 0 : QgsJoinByNearestAlgorithm *QgsJoinByNearestAlgorithm::createInstance() const
114 : : {
115 : 0 : return new QgsJoinByNearestAlgorithm();
116 : : }
117 : :
118 : 0 : QVariantMap QgsJoinByNearestAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
119 : : {
120 : 0 : const int neighbors = parameterAsInt( parameters, QStringLiteral( "NEIGHBORS" ), context );
121 : 0 : const bool discardNonMatching = parameterAsBoolean( parameters, QStringLiteral( "DISCARD_NONMATCHING" ), context );
122 : 0 : const double maxDistance = parameters.value( QStringLiteral( "MAX_DISTANCE" ) ).isValid() ? parameterAsDouble( parameters, QStringLiteral( "MAX_DISTANCE" ), context ) : std::numeric_limits< double >::quiet_NaN();
123 : 0 : std::unique_ptr< QgsProcessingFeatureSource > input( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
124 : 0 : if ( !input )
125 : 0 : throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
126 : :
127 : 0 : std::unique_ptr< QgsProcessingFeatureSource > input2( parameterAsSource( parameters, QStringLiteral( "INPUT_2" ), context ) );
128 : 0 : if ( !input2 )
129 : 0 : throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT_2" ) ) );
130 : :
131 : 0 : const bool sameSourceAndTarget = parameters.value( QStringLiteral( "INPUT" ) ) == parameters.value( QStringLiteral( "INPUT_2" ) );
132 : :
133 : 0 : QString prefix = parameterAsString( parameters, QStringLiteral( "PREFIX" ), context );
134 : 0 : const QStringList fieldsToCopy = parameterAsFields( parameters, QStringLiteral( "FIELDS_TO_COPY" ), context );
135 : :
136 : 0 : QgsFields outFields2;
137 : 0 : QgsAttributeList fields2Indices;
138 : 0 : if ( fieldsToCopy.empty() )
139 : : {
140 : 0 : outFields2 = input2->fields();
141 : 0 : fields2Indices.reserve( outFields2.count() );
142 : 0 : for ( int i = 0; i < outFields2.count(); ++i )
143 : : {
144 : 0 : fields2Indices << i;
145 : 0 : }
146 : 0 : }
147 : : else
148 : : {
149 : 0 : fields2Indices.reserve( fieldsToCopy.count() );
150 : 0 : for ( const QString &field : fieldsToCopy )
151 : : {
152 : 0 : int index = input2->fields().lookupField( field );
153 : 0 : if ( index >= 0 )
154 : : {
155 : 0 : fields2Indices << index;
156 : 0 : outFields2.append( input2->fields().at( index ) );
157 : 0 : }
158 : : }
159 : : }
160 : :
161 : 0 : if ( !prefix.isEmpty() )
162 : : {
163 : 0 : for ( int i = 0; i < outFields2.count(); ++i )
164 : : {
165 : 0 : outFields2.rename( i, prefix + outFields2[ i ].name() );
166 : 0 : }
167 : 0 : }
168 : :
169 : 0 : QgsAttributeList fields2Fetch = fields2Indices;
170 : :
171 : 0 : QgsFields outFields = QgsProcessingUtils::combineFields( input->fields(), outFields2 );
172 : 0 : outFields.append( QgsField( QStringLiteral( "n" ), QVariant::Int ) );
173 : 0 : outFields.append( QgsField( QStringLiteral( "distance" ), QVariant::Double ) );
174 : 0 : outFields.append( QgsField( QStringLiteral( "feature_x" ), QVariant::Double ) );
175 : 0 : outFields.append( QgsField( QStringLiteral( "feature_y" ), QVariant::Double ) );
176 : 0 : outFields.append( QgsField( QStringLiteral( "nearest_x" ), QVariant::Double ) );
177 : 0 : outFields.append( QgsField( QStringLiteral( "nearest_y" ), QVariant::Double ) );
178 : :
179 : 0 : QString dest;
180 : 0 : std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, outFields,
181 : 0 : input->wkbType(), input->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) );
182 : 0 : if ( parameters.value( QStringLiteral( "OUTPUT" ) ).isValid() && !sink )
183 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
184 : :
185 : 0 : QString destNonMatching1;
186 : 0 : std::unique_ptr< QgsFeatureSink > sinkNonMatching1( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING" ), context, destNonMatching1, input->fields(),
187 : 0 : input->wkbType(), input->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) );
188 : 0 : if ( parameters.value( QStringLiteral( "NON_MATCHING" ) ).isValid() && !sinkNonMatching1 )
189 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING" ) ) );
190 : :
191 : : // make spatial index
192 : 0 : QgsFeatureIterator f2 = input2->getFeatures( QgsFeatureRequest().setDestinationCrs( input->sourceCrs(), context.transformContext() ).setSubsetOfAttributes( fields2Fetch ) );
193 : 0 : QHash< QgsFeatureId, QgsAttributes > input2AttributeCache;
194 : 0 : double step = input2->featureCount() > 0 ? 50.0 / input2->featureCount() : 1;
195 : 0 : int i = 0;
196 : 0 : QgsSpatialIndex index( f2, [&]( const QgsFeature & f )->bool
197 : : {
198 : 0 : i++;
199 : 0 : if ( feedback->isCanceled() )
200 : 0 : return false;
201 : :
202 : 0 : feedback->setProgress( i * step );
203 : :
204 : 0 : if ( !f.hasGeometry() )
205 : 0 : return true;
206 : :
207 : : // only keep selected attributes
208 : 0 : QgsAttributes attributes;
209 : 0 : for ( int j = 0; j < f.attributes().count(); ++j )
210 : : {
211 : 0 : if ( ! fields2Indices.contains( j ) )
212 : 0 : continue;
213 : 0 : attributes << f.attribute( j );
214 : 0 : }
215 : 0 : input2AttributeCache.insert( f.id(), attributes );
216 : :
217 : 0 : return true;
218 : 0 : }, QgsSpatialIndex::FlagStoreFeatureGeometries );
219 : :
220 : 0 : QgsFeature f;
221 : :
222 : : // create extra null attributes for non-matched records (the +2 is for the "n" and "distance", and start/end x/y fields)
223 : 0 : QgsAttributes nullMatch;
224 : 0 : nullMatch.reserve( fields2Indices.size() + 6 );
225 : 0 : for ( int i = 0; i < fields2Indices.count() + 6; ++i )
226 : 0 : nullMatch << QVariant();
227 : :
228 : 0 : long long joinedCount = 0;
229 : 0 : long long unjoinedCount = 0;
230 : :
231 : : // Create output vector layer with additional attributes
232 : 0 : step = input->featureCount() > 0 ? 50.0 / input->featureCount() : 1;
233 : 0 : QgsFeatureIterator features = input->getFeatures();
234 : 0 : i = 0;
235 : 0 : while ( features.nextFeature( f ) )
236 : : {
237 : 0 : i++;
238 : 0 : if ( feedback->isCanceled() )
239 : : {
240 : 0 : break;
241 : : }
242 : :
243 : 0 : feedback->setProgress( 50 + i * step );
244 : :
245 : 0 : if ( !f.hasGeometry() )
246 : : {
247 : 0 : unjoinedCount++;
248 : 0 : if ( sinkNonMatching1 )
249 : : {
250 : 0 : sinkNonMatching1->addFeature( f, QgsFeatureSink::FastInsert );
251 : 0 : }
252 : 0 : if ( sink && !discardNonMatching )
253 : : {
254 : 0 : QgsAttributes attr = f.attributes();
255 : 0 : attr.append( nullMatch );
256 : 0 : f.setAttributes( attr );
257 : 0 : sink->addFeature( f, QgsFeatureSink::FastInsert );
258 : 0 : }
259 : 0 : }
260 : : else
261 : : {
262 : : // note - if using same source as target, we have to get one extra neighbor, since the first match will be the input feature
263 : :
264 : : // if the user didn't specify a distance (isnan), then use 0 for nearestNeighbor() parameter
265 : : // if the user specified 0 exactly, then use the smallest positive double value instead
266 : 0 : const double searchDistance = std::isnan( maxDistance ) ? 0 : std::max( std::numeric_limits<double>::min(), maxDistance );
267 : 0 : const QList< QgsFeatureId > nearest = index.nearestNeighbor( f.geometry(), neighbors + ( sameSourceAndTarget ? 1 : 0 ), searchDistance );
268 : :
269 : 0 : if ( nearest.count() > neighbors + ( sameSourceAndTarget ? 1 : 0 ) )
270 : : {
271 : 0 : feedback->pushInfo( QObject::tr( "Multiple matching features found at same distance from search feature, found %1 features instead of %2" ).arg( nearest.count() - ( sameSourceAndTarget ? 1 : 0 ) ).arg( neighbors ) );
272 : 0 : }
273 : 0 : QgsFeature out;
274 : 0 : out.setGeometry( f.geometry() );
275 : 0 : int j = 0;
276 : 0 : for ( QgsFeatureId id : nearest )
277 : : {
278 : 0 : if ( sameSourceAndTarget && id == f.id() )
279 : 0 : continue; // don't match to same feature if using a single input table
280 : 0 : j++;
281 : 0 : if ( sink )
282 : : {
283 : 0 : QgsAttributes attr = f.attributes();
284 : 0 : attr.append( input2AttributeCache.value( id ) );
285 : 0 : attr.append( j );
286 : :
287 : 0 : const QgsGeometry closestLine = f.geometry().shortestLine( index.geometry( id ) );
288 : 0 : if ( const QgsLineString *line = qgsgeometry_cast< const QgsLineString *>( closestLine.constGet() ) )
289 : : {
290 : 0 : attr.append( line->length() );
291 : 0 : attr.append( line->startPoint().x() );
292 : 0 : attr.append( line->startPoint().y() );
293 : 0 : attr.append( line->endPoint().x() );
294 : 0 : attr.append( line->endPoint().y() );
295 : 0 : }
296 : : else
297 : : {
298 : 0 : attr.append( QVariant() ); //distance
299 : 0 : attr.append( QVariant() ); //start x
300 : 0 : attr.append( QVariant() ); //start y
301 : 0 : attr.append( QVariant() ); //end x
302 : 0 : attr.append( QVariant() ); //end y
303 : : }
304 : 0 : out.setAttributes( attr );
305 : 0 : sink->addFeature( out, QgsFeatureSink::FastInsert );
306 : 0 : }
307 : : }
308 : 0 : if ( j > 0 )
309 : 0 : joinedCount++;
310 : : else
311 : : {
312 : 0 : if ( sinkNonMatching1 )
313 : : {
314 : 0 : sinkNonMatching1->addFeature( f, QgsFeatureSink::FastInsert );
315 : 0 : }
316 : 0 : if ( !discardNonMatching && sink )
317 : : {
318 : 0 : QgsAttributes attr = f.attributes();
319 : 0 : attr.append( nullMatch );
320 : 0 : f.setAttributes( attr );
321 : 0 : sink->addFeature( f, QgsFeatureSink::FastInsert );
322 : 0 : }
323 : 0 : unjoinedCount++;
324 : : }
325 : 0 : }
326 : : }
327 : :
328 : 0 : QVariantMap outputs;
329 : 0 : outputs.insert( QStringLiteral( "JOINED_COUNT" ), joinedCount );
330 : 0 : outputs.insert( QStringLiteral( "UNJOINABLE_COUNT" ), unjoinedCount );
331 : 0 : if ( sink )
332 : 0 : outputs.insert( QStringLiteral( "OUTPUT" ), dest );
333 : 0 : if ( sinkNonMatching1 )
334 : 0 : outputs.insert( QStringLiteral( "NON_MATCHING" ), destNonMatching1 );
335 : 0 : return outputs;
336 : 0 : }
337 : :
338 : :
339 : : ///@endcond
|