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