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