24 QString QgsDetectVectorChangesAlgorithm::name()
const
26 return QStringLiteral(
"detectvectorchanges" );
29 QString QgsDetectVectorChangesAlgorithm::displayName()
const
31 return QObject::tr(
"Detect dataset changes" );
34 QStringList QgsDetectVectorChangesAlgorithm::tags()
const
36 return QObject::tr(
"added,dropped,new,deleted,features,geometries,difference,delta,revised,original,version" ).split(
',' );
39 QString QgsDetectVectorChangesAlgorithm::group()
const
41 return QObject::tr(
"Vector general" );
44 QString QgsDetectVectorChangesAlgorithm::groupId()
const
46 return QStringLiteral(
"vectorgeneral" );
49 void QgsDetectVectorChangesAlgorithm::initAlgorithm(
const QVariantMap & )
54 std::unique_ptr< QgsProcessingParameterField > compareAttributesParam = std::make_unique< QgsProcessingParameterField >( QStringLiteral(
"COMPARE_ATTRIBUTES" ),
55 QObject::tr(
"Attributes to consider for match (or none to compare geometry only)" ), QVariant(),
57 compareAttributesParam->setDefaultToAllFields(
true );
58 addParameter( compareAttributesParam.release() );
60 std::unique_ptr< QgsProcessingParameterDefinition > matchTypeParam = std::make_unique< QgsProcessingParameterEnum >( QStringLiteral(
"MATCH_TYPE" ),
61 QObject::tr(
"Geometry comparison behavior" ),
62 QStringList() << QObject::tr(
"Exact Match" )
63 << QObject::tr(
"Tolerant Match (Topological Equality)" ),
66 addParameter( matchTypeParam.release() );
72 addOutput(
new QgsProcessingOutputNumber( QStringLiteral(
"UNCHANGED_COUNT" ), QObject::tr(
"Count of unchanged features" ) ) );
73 addOutput(
new QgsProcessingOutputNumber( QStringLiteral(
"ADDED_COUNT" ), QObject::tr(
"Count of features added in revised layer" ) ) );
74 addOutput(
new QgsProcessingOutputNumber( QStringLiteral(
"DELETED_COUNT" ), QObject::tr(
"Count of features deleted from original layer" ) ) );
77 QString QgsDetectVectorChangesAlgorithm::shortHelpString()
const
79 return QObject::tr(
"This algorithm compares two vector layers, and determines which features are unchanged, added or deleted between "
80 "the two. It is designed for comparing two different versions of the same dataset.\n\n"
81 "When comparing features, the original and revised feature geometries will be compared against each other. Depending "
82 "on the Geometry Comparison Behavior setting, the comparison will either be made using an exact comparison (where "
83 "geometries must be an exact match for each other, including the order and count of vertices) or a topological "
84 "comparison only (where geometries are considered equal if all of their component edges overlap. E.g. "
85 "lines with the same vertex locations but opposite direction will be considered equal by this method). If the topological "
86 "comparison is selected then any z or m values present in the geometries will not be compared.\n\n"
87 "By default, the algorithm compares all attributes from the original and revised features. If the Attributes to Consider for Match "
88 "parameter is changed, then only the selected attributes will be compared (e.g. allowing users to ignore a timestamp or ID field "
89 "which is expected to change between the revisions).\n\n"
90 "If any features in the original or revised layers do not have an associated geometry, then care must be taken to ensure "
91 "that these features have a unique set of attributes selected for comparison. If this condition is not met, warnings will be "
92 "raised and the resultant outputs may be misleading.\n\n"
93 "The algorithm outputs three layers, one containing all features which are considered to be unchanged between the revisions, "
94 "one containing features deleted from the original layer which are not present in the revised layer, and one containing features "
95 "added to the revised layer which are not present in the original layer." );
98 QString QgsDetectVectorChangesAlgorithm::shortDescription()
const
100 return QObject::tr(
"Calculates features which are unchanged, added or deleted between two dataset versions." );
103 QgsDetectVectorChangesAlgorithm *QgsDetectVectorChangesAlgorithm::createInstance()
const
105 return new QgsDetectVectorChangesAlgorithm();
110 mOriginal.reset( parameterAsSource( parameters, QStringLiteral(
"ORIGINAL" ), context ) );
114 mRevised.reset( parameterAsSource( parameters, QStringLiteral(
"REVISED" ), context ) );
118 mMatchType =
static_cast< GeometryMatchType
>( parameterAsEnum( parameters, QStringLiteral(
"MATCH_TYPE" ), context ) );
120 switch ( mMatchType )
123 if ( mOriginal->wkbType() != mRevised->wkbType() )
136 if ( mOriginal->sourceCrs() != mRevised->sourceCrs() )
137 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(),
138 mRevised->sourceCrs().userFriendlyIdentifier() ), false );
140 mFieldsToCompare = parameterAsFields( parameters, QStringLiteral(
"COMPARE_ATTRIBUTES" ), context );
141 mOriginalFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
142 mRevisedFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
143 QStringList missingOriginalFields;
144 QStringList missingRevisedFields;
145 for (
const QString &
field : mFieldsToCompare )
147 const int originalIndex = mOriginal->fields().lookupField(
field );
148 mOriginalFieldsToCompareIndices.append( originalIndex );
149 if ( originalIndex < 0 )
150 missingOriginalFields <<
field;
152 const int revisedIndex = mRevised->fields().lookupField(
field );
153 if ( revisedIndex < 0 )
154 missingRevisedFields <<
field;
155 mRevisedFieldsToCompareIndices.append( revisedIndex );
158 if ( !missingOriginalFields.empty() )
159 throw QgsProcessingException( QObject::tr(
"Original layer missing selected comparison attributes: %1" ).arg( missingOriginalFields.join(
',' ) ) );
160 if ( !missingRevisedFields.empty() )
161 throw QgsProcessingException( QObject::tr(
"Revised layer missing selected comparison attributes: %1" ).arg( missingRevisedFields.join(
',' ) ) );
168 QString unchangedDestId;
169 std::unique_ptr< QgsFeatureSink > unchangedSink( parameterAsSink( parameters, QStringLiteral(
"UNCHANGED" ), context, unchangedDestId, mOriginal->fields(),
170 mOriginal->wkbType(), mOriginal->sourceCrs() ) );
171 if ( !unchangedSink && parameters.value( QStringLiteral(
"UNCHANGED" ) ).isValid() )
175 std::unique_ptr< QgsFeatureSink > addedSink( parameterAsSink( parameters, QStringLiteral(
"ADDED" ), context, addedDestId, mRevised->fields(),
176 mRevised->wkbType(), mRevised->sourceCrs() ) );
177 if ( !addedSink && parameters.value( QStringLiteral(
"ADDED" ) ).isValid() )
180 QString deletedDestId;
181 std::unique_ptr< QgsFeatureSink > deletedSink( parameterAsSink( parameters, QStringLiteral(
"DELETED" ), context, deletedDestId, mOriginal->fields(),
182 mOriginal->wkbType(), mOriginal->sourceCrs() ) );
183 if ( !deletedSink && parameters.value( QStringLiteral(
"DELETED" ) ).isValid() )
193 double step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
194 QHash< QgsFeatureId, QgsGeometry > originalGeometries;
195 QHash< QgsFeatureId, QgsAttributes > originalAttributes;
196 QHash< QgsAttributes, QgsFeatureId > originalNullGeometryAttributes;
200 attrs.resize( mFieldsToCompare.size() );
209 originalGeometries.insert( f.id(), f.geometry() );
212 if ( !mFieldsToCompare.empty() )
215 for ( const int field : mOriginalFieldsToCompareIndices )
217 attrs[idx++] = f.attributes().at( field );
219 originalAttributes.insert( f.
id(), attrs );
224 if ( originalNullGeometryAttributes.contains( attrs ) )
226 feedback->reportError( QObject::tr(
"A non-unique set of comparison attributes was found for "
227 "one or more features without geometries - results may be misleading (features %1 and %2)" ).arg( f.id() ).arg( originalNullGeometryAttributes.value( attrs ) ) );
231 originalNullGeometryAttributes.insert( attrs, f.id() );
241 QSet<QgsFeatureId> unchangedOriginalIds;
242 QSet<QgsFeatureId> addedRevisedIds;
247 step = mRevised->featureCount() > 0 ? 100.0 / mRevised->featureCount() : 0;
250 it = mRevised->getFeatures( revisedRequest );
258 for (
const int field : mRevisedFieldsToCompareIndices )
263 bool matched =
false;
267 if ( originalNullGeometryAttributes.contains( attrs ) )
270 unchangedOriginalIds.insert( originalNullGeometryAttributes.value( attrs ) );
277 const QList<QgsFeatureId> candidates = index.intersects( revisedFeature.
geometry().
boundingBox() );
284 if ( unchangedOriginalIds.contains( candidateId ) )
291 if ( !mFieldsToCompare.empty() )
293 if ( attrs != originalAttributes[ candidateId ] )
300 QgsGeometry original = originalGeometries.value( candidateId );
304 revised = revisedFeature.
geometry();
306 switch ( mMatchType )
322 bool geometryMatch =
false;
323 switch ( mMatchType )
332 geometryMatch = revised.
equals( original );
339 unchangedOriginalIds.insert( candidateId );
349 addedRevisedIds.insert( revisedFeature.
id() );
353 feedback->
setProgress( 0.70 * current * step + 10 );
359 step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
362 it = mOriginal->getFeatures( request );
374 if ( unchangedOriginalIds.contains( f.
id() ) )
380 throw QgsProcessingException( writeFeatureError( unchangedSink.get(), parameters, QStringLiteral(
"UNCHANGED" ) ) );
389 throw QgsProcessingException( writeFeatureError( deletedSink.get(), parameters, QStringLiteral(
"DELETED" ) ) );
395 feedback->
setProgress( 0.10 * current * step + 80 );
406 step = addedRevisedIds.size() > 0 ? 100.0 / addedRevisedIds.size() : 0;
407 it = mRevised->getFeatures(
QgsFeatureRequest().setFilterFids( addedRevisedIds ) );
416 throw QgsProcessingException( writeFeatureError( addedSink.get(), parameters, QStringLiteral(
"ADDED" ) ) );
419 feedback->
setProgress( 0.10 * current * step + 90 );
424 feedback->
pushInfo( QObject::tr(
"%n feature(s) unchanged",
nullptr, unchangedOriginalIds.size() ) );
425 feedback->
pushInfo( QObject::tr(
"%n feature(s) added",
nullptr, addedRevisedIds.size() ) );
426 feedback->
pushInfo( QObject::tr(
"%n feature(s) deleted",
nullptr, deleted ) );
429 outputs.insert( QStringLiteral(
"UNCHANGED" ), unchangedDestId );
430 outputs.insert( QStringLiteral(
"ADDED" ), addedDestId );
431 outputs.insert( QStringLiteral(
"DELETED" ), deletedDestId );
432 outputs.insert( QStringLiteral(
"UNCHANGED_COUNT" ),
static_cast< long long >( unchangedOriginalIds.size() ) );
433 outputs.insert( QStringLiteral(
"ADDED_COUNT" ),
static_cast< long long >( addedRevisedIds.size() ) );
434 outputs.insert( QStringLiteral(
"DELETED_COUNT" ),
static_cast< long long >( deleted ) );