QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
qgsalgorithmdetectdatasetchanges.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmdetectdatasetchanges.cpp
3 -----------------------------------------
4 begin : December 2019
5 copyright : (C) 2019 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
19
20#include "qgsspatialindex.h"
21#include "qgsvectorlayer.h"
22
24
25QString QgsDetectVectorChangesAlgorithm::name() const
26{
27 return QStringLiteral( "detectvectorchanges" );
28}
29
30QString QgsDetectVectorChangesAlgorithm::displayName() const
31{
32 return QObject::tr( "Detect dataset changes" );
33}
34
35QStringList QgsDetectVectorChangesAlgorithm::tags() const
36{
37 return QObject::tr( "added,dropped,new,deleted,features,geometries,difference,delta,revised,original,version" ).split( ',' );
38}
39
40QString QgsDetectVectorChangesAlgorithm::group() const
41{
42 return QObject::tr( "Vector general" );
43}
44
45QString QgsDetectVectorChangesAlgorithm::groupId() const
46{
47 return QStringLiteral( "vectorgeneral" );
48}
49
50void QgsDetectVectorChangesAlgorithm::initAlgorithm( const QVariantMap & )
51{
52 addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "ORIGINAL" ), QObject::tr( "Original layer" ) ) );
53 addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "REVISED" ), QObject::tr( "Revised layer" ) ) );
54
55 auto compareAttributesParam = std::make_unique<QgsProcessingParameterField>( QStringLiteral( "COMPARE_ATTRIBUTES" ), QObject::tr( "Attributes to consider for match (or none to compare geometry only)" ), QVariant(), QStringLiteral( "ORIGINAL" ), Qgis::ProcessingFieldParameterDataType::Any, true, true );
56 compareAttributesParam->setDefaultToAllFields( true );
57 addParameter( compareAttributesParam.release() );
58
59 std::unique_ptr<QgsProcessingParameterDefinition> matchTypeParam = std::make_unique<QgsProcessingParameterEnum>( QStringLiteral( "MATCH_TYPE" ), QObject::tr( "Geometry comparison behavior" ), QStringList() << QObject::tr( "Exact Match" ) << QObject::tr( "Tolerant Match (Topological Equality)" ), false, 1 );
60 matchTypeParam->setFlags( matchTypeParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
61 addParameter( matchTypeParam.release() );
62
63 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "UNCHANGED" ), QObject::tr( "Unchanged features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
64 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "ADDED" ), QObject::tr( "Added features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
65 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "DELETED" ), QObject::tr( "Deleted features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
66
67 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNCHANGED_COUNT" ), QObject::tr( "Count of unchanged features" ) ) );
68 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "ADDED_COUNT" ), QObject::tr( "Count of features added in revised layer" ) ) );
69 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "DELETED_COUNT" ), QObject::tr( "Count of features deleted from original layer" ) ) );
70}
71
72QString QgsDetectVectorChangesAlgorithm::shortHelpString() const
73{
74 return QObject::tr( "This algorithm compares two vector layers, and determines which features are unchanged, added or deleted between "
75 "the two. It is designed for comparing two different versions of the same dataset.\n\n"
76 "When comparing features, the original and revised feature geometries will be compared against each other. Depending "
77 "on the Geometry Comparison Behavior setting, the comparison will either be made using an exact comparison (where "
78 "geometries must be an exact match for each other, including the order and count of vertices) or a topological "
79 "comparison only (where geometries are considered equal if all of their component edges overlap. E.g. "
80 "lines with the same vertex locations but opposite direction will be considered equal by this method). If the topological "
81 "comparison is selected then any z or m values present in the geometries will not be compared.\n\n"
82 "By default, the algorithm compares all attributes from the original and revised features. If the Attributes to Consider for Match "
83 "parameter is changed, then only the selected attributes will be compared (e.g. allowing users to ignore a timestamp or ID field "
84 "which is expected to change between the revisions).\n\n"
85 "If any features in the original or revised layers do not have an associated geometry, then care must be taken to ensure "
86 "that these features have a unique set of attributes selected for comparison. If this condition is not met, warnings will be "
87 "raised and the resultant outputs may be misleading.\n\n"
88 "The algorithm outputs three layers, one containing all features which are considered to be unchanged between the revisions, "
89 "one containing features deleted from the original layer which are not present in the revised layer, and one containing features "
90 "added to the revised layer which are not present in the original layer." );
91}
92
93QString QgsDetectVectorChangesAlgorithm::shortDescription() const
94{
95 return QObject::tr( "Calculates features which are unchanged, added or deleted between two dataset versions." );
96}
97
98QgsDetectVectorChangesAlgorithm *QgsDetectVectorChangesAlgorithm::createInstance() const
99{
100 return new QgsDetectVectorChangesAlgorithm();
101}
102
103bool QgsDetectVectorChangesAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
104{
105 mOriginal.reset( parameterAsSource( parameters, QStringLiteral( "ORIGINAL" ), context ) );
106 if ( !mOriginal )
107 throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "ORIGINAL" ) ) );
108
109 mRevised.reset( parameterAsSource( parameters, QStringLiteral( "REVISED" ), context ) );
110 if ( !mRevised )
111 throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "REVISED" ) ) );
112
113 mMatchType = static_cast<GeometryMatchType>( parameterAsEnum( parameters, QStringLiteral( "MATCH_TYPE" ), context ) );
114
115 switch ( mMatchType )
116 {
117 case Exact:
118 if ( mOriginal->wkbType() != mRevised->wkbType() )
119 throw QgsProcessingException( QObject::tr( "Geometry type of revised layer (%1) does not match the original layer (%2). Consider using the \"Tolerant Match\" option instead." ).arg( QgsWkbTypes::displayString( mRevised->wkbType() ), QgsWkbTypes::displayString( mOriginal->wkbType() ) ) );
120 break;
121
122 case Topological:
123 if ( QgsWkbTypes::geometryType( mOriginal->wkbType() ) != QgsWkbTypes::geometryType( mRevised->wkbType() ) )
124 throw QgsProcessingException( QObject::tr( "Geometry type of revised layer (%1) does not match the original layer (%2)" ).arg( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( mRevised->wkbType() ) ), QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( mOriginal->wkbType() ) ) ) );
125 break;
126 }
127
128 if ( mOriginal->sourceCrs() != mRevised->sourceCrs() )
129 feedback->reportError( QObject::tr( "CRS for revised layer (%1) does not match the original layer (%2) - reprojection accuracy may affect geometry matching" ).arg( mOriginal->sourceCrs().userFriendlyIdentifier(), mRevised->sourceCrs().userFriendlyIdentifier() ), false );
130
131 mFieldsToCompare = parameterAsStrings( parameters, QStringLiteral( "COMPARE_ATTRIBUTES" ), context );
132 mOriginalFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
133 mRevisedFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
134 QStringList missingOriginalFields;
135 QStringList missingRevisedFields;
136 for ( const QString &field : mFieldsToCompare )
137 {
138 const int originalIndex = mOriginal->fields().lookupField( field );
139 mOriginalFieldsToCompareIndices.append( originalIndex );
140 if ( originalIndex < 0 )
141 missingOriginalFields << field;
142
143 const int revisedIndex = mRevised->fields().lookupField( field );
144 if ( revisedIndex < 0 )
145 missingRevisedFields << field;
146 mRevisedFieldsToCompareIndices.append( revisedIndex );
147 }
148
149 if ( !missingOriginalFields.empty() )
150 throw QgsProcessingException( QObject::tr( "Original layer missing selected comparison attributes: %1" ).arg( missingOriginalFields.join( ',' ) ) );
151 if ( !missingRevisedFields.empty() )
152 throw QgsProcessingException( QObject::tr( "Revised layer missing selected comparison attributes: %1" ).arg( missingRevisedFields.join( ',' ) ) );
153
154 return true;
155}
156
157QVariantMap QgsDetectVectorChangesAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
158{
159 QString unchangedDestId;
160 std::unique_ptr<QgsFeatureSink> unchangedSink( parameterAsSink( parameters, QStringLiteral( "UNCHANGED" ), context, unchangedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
161 if ( !unchangedSink && parameters.value( QStringLiteral( "UNCHANGED" ) ).isValid() )
162 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "UNCHANGED" ) ) );
163
164 QString addedDestId;
165 std::unique_ptr<QgsFeatureSink> addedSink( parameterAsSink( parameters, QStringLiteral( "ADDED" ), context, addedDestId, mRevised->fields(), mRevised->wkbType(), mRevised->sourceCrs() ) );
166 if ( !addedSink && parameters.value( QStringLiteral( "ADDED" ) ).isValid() )
167 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "ADDED" ) ) );
168
169 QString deletedDestId;
170 std::unique_ptr<QgsFeatureSink> deletedSink( parameterAsSink( parameters, QStringLiteral( "DELETED" ), context, deletedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
171 if ( !deletedSink && parameters.value( QStringLiteral( "DELETED" ) ).isValid() )
172 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "DELETED" ) ) );
173
174 // first iteration: we loop through the entire original layer, building up a spatial index of ALL original geometries
175 // and collecting the original geometries themselves along with the attributes to compare
176 QgsFeatureRequest request;
177 request.setSubsetOfAttributes( mOriginalFieldsToCompareIndices );
178
179 QgsFeatureIterator it = mOriginal->getFeatures( request );
180
181 double step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
182 QHash<QgsFeatureId, QgsGeometry> originalGeometries;
183 QHash<QgsFeatureId, QgsAttributes> originalAttributes;
184 QHash<QgsAttributes, QgsFeatureId> originalNullGeometryAttributes;
185 long current = 0;
186
187 QgsAttributes attrs;
188 attrs.resize( mFieldsToCompare.size() );
189
190 const QgsSpatialIndex index( it, [&]( const QgsFeature &f ) -> bool {
191 if ( feedback->isCanceled() )
192 return false;
193
194 if ( f.hasGeometry() )
195 {
196 originalGeometries.insert( f.id(), f.geometry() );
197 }
198
199 if ( !mFieldsToCompare.empty() )
200 {
201 int idx = 0;
202 for ( const int field : mOriginalFieldsToCompareIndices )
203 {
204 attrs[idx++] = f.attributes().at( field );
205 }
206 originalAttributes.insert( f.id(), attrs );
207 }
208
209 if ( !f.hasGeometry() )
210 {
211 if ( originalNullGeometryAttributes.contains( attrs ) )
212 {
213 feedback->reportError( QObject::tr( "A non-unique set of comparison attributes was found for "
214 "one or more features without geometries - results may be misleading (features %1 and %2)" )
215 .arg( f.id() )
216 .arg( originalNullGeometryAttributes.value( attrs ) ) );
217 }
218 else
219 {
220 originalNullGeometryAttributes.insert( attrs, f.id() );
221 }
222 }
223
224 // overall this loop takes about 10% of time
225 current++;
226 feedback->setProgress( 0.10 * current * step );
227 return true;
228 } );
229
230 QSet<QgsFeatureId> unchangedOriginalIds;
231 QSet<QgsFeatureId> addedRevisedIds;
232 current = 0;
233
234 // second iteration: we loop through ALL revised features, checking whether each is a match for a geometry from the
235 // original set. If so, check if the feature is unchanged. If there's no match with the original features, we mark it as an "added" feature
236 step = mRevised->featureCount() > 0 ? 100.0 / mRevised->featureCount() : 0;
237 QgsFeatureRequest revisedRequest = QgsFeatureRequest().setDestinationCrs( mOriginal->sourceCrs(), context.transformContext() );
238 revisedRequest.setSubsetOfAttributes( mRevisedFieldsToCompareIndices );
239 it = mRevised->getFeatures( revisedRequest );
240 QgsFeature revisedFeature;
241 while ( it.nextFeature( revisedFeature ) )
242 {
243 if ( feedback->isCanceled() )
244 break;
245
246 int idx = 0;
247 for ( const int field : mRevisedFieldsToCompareIndices )
248 {
249 attrs[idx++] = revisedFeature.attributes().at( field );
250 }
251
252 bool matched = false;
253
254 if ( !revisedFeature.hasGeometry() )
255 {
256 if ( originalNullGeometryAttributes.contains( attrs ) )
257 {
258 // found a match for feature
259 unchangedOriginalIds.insert( originalNullGeometryAttributes.value( attrs ) );
260 matched = true;
261 }
262 }
263 else
264 {
265 // can we match this feature?
266 const QList<QgsFeatureId> candidates = index.intersects( revisedFeature.geometry().boundingBox() );
267
268 // lazy evaluate -- there may be NO candidates!
269 QgsGeometry revised;
270
271 for ( const QgsFeatureId candidateId : candidates )
272 {
273 if ( unchangedOriginalIds.contains( candidateId ) )
274 {
275 // already matched this original feature
276 continue;
277 }
278
279 // attribute comparison is faster to do first, if desired
280 if ( !mFieldsToCompare.empty() )
281 {
282 if ( attrs != originalAttributes[candidateId] )
283 {
284 // attributes don't match, so candidates is not a match
285 continue;
286 }
287 }
288
289 QgsGeometry original = originalGeometries.value( candidateId );
290 // lazy evaluation
291 if ( revised.isNull() )
292 {
293 revised = revisedFeature.geometry();
294 // drop z/m if not wanted for match
295 switch ( mMatchType )
296 {
297 case Topological:
298 {
299 revised.get()->dropMValue();
300 revised.get()->dropZValue();
301 original.get()->dropMValue();
302 original.get()->dropZValue();
303 break;
304 }
305
306 case Exact:
307 break;
308 }
309 }
310
311 bool geometryMatch = false;
312 switch ( mMatchType )
313 {
314 case Topological:
315 {
316 geometryMatch = revised.isGeosEqual( original );
317 break;
318 }
319
320 case Exact:
321 geometryMatch = revised.equals( original );
322 break;
323 }
324
325 if ( geometryMatch )
326 {
327 // candidate is a match for feature
328 unchangedOriginalIds.insert( candidateId );
329 matched = true;
330 break;
331 }
332 }
333 }
334
335 if ( !matched )
336 {
337 // new feature
338 addedRevisedIds.insert( revisedFeature.id() );
339 }
340
341 current++;
342 feedback->setProgress( 0.70 * current * step + 10 ); // takes about 70% of time
343 }
344
345 // third iteration: iterate back over the original features, and direct them to the appropriate sink.
346 // If they were marked as unchanged during the second iteration, we put them in the unchanged sink. Otherwise
347 // they are placed into the deleted sink.
348 step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
349
351 it = mOriginal->getFeatures( request );
352 current = 0;
353 long deleted = 0;
354 QgsFeature f;
355 while ( it.nextFeature( f ) )
356 {
357 if ( feedback->isCanceled() )
358 break;
359
360 // use already fetched geometry
361 f.setGeometry( originalGeometries.value( f.id(), QgsGeometry() ) );
362
363 if ( unchangedOriginalIds.contains( f.id() ) )
364 {
365 // unchanged
366 if ( unchangedSink )
367 {
368 if ( !unchangedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
369 throw QgsProcessingException( writeFeatureError( unchangedSink.get(), parameters, QStringLiteral( "UNCHANGED" ) ) );
370 }
371 }
372 else
373 {
374 // deleted feature
375 if ( deletedSink )
376 {
377 if ( !deletedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
378 throw QgsProcessingException( writeFeatureError( deletedSink.get(), parameters, QStringLiteral( "DELETED" ) ) );
379 }
380 deleted++;
381 }
382
383 current++;
384 feedback->setProgress( 0.10 * current * step + 80 ); // takes about 10% of time
385 }
386
387 // forth iteration: collect all added features and add them to the added sink
388 // NOTE: while we could potentially do this as part of the second iteration and save some time, we instead
389 // do this here using a brand new request because the second iteration
390 // is fetching reprojected features and we ideally want geometries from the revised layer's actual CRS only here!
391 // also, the second iteration is only fetching the actual attributes used in the comparison, whereas we want
392 // to include all attributes in the "added" output
393 if ( addedSink )
394 {
395 step = addedRevisedIds.size() > 0 ? 100.0 / addedRevisedIds.size() : 0;
396 it = mRevised->getFeatures( QgsFeatureRequest().setFilterFids( addedRevisedIds ) );
397 current = 0;
398 while ( it.nextFeature( f ) )
399 {
400 if ( feedback->isCanceled() )
401 break;
402
403 // added feature
404 if ( !addedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
405 throw QgsProcessingException( writeFeatureError( addedSink.get(), parameters, QStringLiteral( "ADDED" ) ) );
406
407 current++;
408 feedback->setProgress( 0.10 * current * step + 90 ); // takes about 10% of time
409 }
410 }
411 feedback->setProgress( 100 );
412
413 feedback->pushInfo( QObject::tr( "%n feature(s) unchanged", nullptr, unchangedOriginalIds.size() ) );
414 feedback->pushInfo( QObject::tr( "%n feature(s) added", nullptr, addedRevisedIds.size() ) );
415 feedback->pushInfo( QObject::tr( "%n feature(s) deleted", nullptr, deleted ) );
416
417 if ( unchangedSink )
418 unchangedSink->finalize();
419 if ( addedSink )
420 addedSink->finalize();
421 if ( deletedSink )
422 deletedSink->finalize();
423
424 QVariantMap outputs;
425 outputs.insert( QStringLiteral( "UNCHANGED" ), unchangedDestId );
426 outputs.insert( QStringLiteral( "ADDED" ), addedDestId );
427 outputs.insert( QStringLiteral( "DELETED" ), deletedDestId );
428 outputs.insert( QStringLiteral( "UNCHANGED_COUNT" ), static_cast<long long>( unchangedOriginalIds.size() ) );
429 outputs.insert( QStringLiteral( "ADDED_COUNT" ), static_cast<long long>( addedRevisedIds.size() ) );
430 outputs.insert( QStringLiteral( "DELETED_COUNT" ), static_cast<long long>( deleted ) );
431
432 return outputs;
433}
434
@ VectorAnyGeometry
Any vector layer with geometry.
Definition qgis.h:3533
@ NoGeometry
Geometry is not required. It may still be returned if e.g. required for a filter condition.
Definition qgis.h:2196
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:3763
virtual bool dropMValue()=0
Drops any measure values which exist in the geometry.
virtual bool dropZValue()=0
Drops any z-dimensions which exist in the geometry.
A vector of attributes.
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setFlags(Qgis::FeatureRequestFlags flags)
Sets flags that affect how features will be fetched.
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
QgsFeatureRequest & setDestinationCrs(const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context)
Sets the destination crs for feature's geometries.
@ 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:58
QgsAttributes attributes
Definition qgsfeature.h:67
QgsFeatureId id
Definition qgsfeature.h:66
QgsGeometry geometry
Definition qgsfeature.h:69
bool hasGeometry() const
Returns true if the feature has an associated geometry.
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
A geometry is the spatial representation of a feature.
QgsAbstractGeometry * get()
Returns a modifiable (non-const) reference to the underlying abstract geometry primitive.
bool equals(const QgsGeometry &geometry) const
Test if this geometry is exactly equal to another geometry.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
bool isGeosEqual(const QgsGeometry &) const
Compares the geometry with another geometry using GEOS.
Contains information about the context in which a processing algorithm is executed.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
Custom exception class for processing related exceptions.
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 reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
A numeric output for processing algorithms.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A spatial index for QgsFeature objects.
static Qgis::GeometryType geometryType(Qgis::WkbType type)
Returns the geometry type for a WKB type, e.g., both MultiPolygon and CurvePolygon would have a Polyg...
static Q_INVOKABLE QString displayString(Qgis::WkbType type)
Returns a non-translated display string type for a WKB type, e.g., the geometry name used in WKT geom...
static Q_INVOKABLE QString geometryDisplayString(Qgis::GeometryType type)
Returns a display string for a geometry type.
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features