QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
qgsgeometrychecker.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * qgsgeometrychecker.cpp *
3  * ------------------- *
4  * copyright : (C) 2014 by Sandro Mani / Sourcepole AG *
5  * email : [email protected] *
6  ***************************************************************************/
7 
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
17 #include <QtConcurrentMap>
18 #include <QFutureWatcher>
19 #include <QMutex>
20 #include <QTimer>
21 #include <QTextStream>
22 
24 #include "qgsgeometrychecker.h"
25 #include "qgsgeometrycheck.h"
26 #include "qgsfeaturepool.h"
27 #include "qgsproject.h"
28 #include "qgsvectorlayer.h"
29 #include "qgsgeometrycheckerror.h"
30 
31 
32 
33 QgsGeometryChecker::QgsGeometryChecker( const QList<QgsGeometryCheck *> &checks, QgsGeometryCheckContext *context, const QMap<QString, QgsFeaturePool *> &featurePools )
34  : mChecks( checks )
35  , mContext( context )
36  , mFeaturePools( featurePools )
37 {
38  for ( auto it = featurePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
39  {
40  if ( it.value()->layer() )
41  {
42  it.value()->layer()->setReadOnly( true );
43  // Enter update mode to defer ogr dataset repacking until the checker has finished
44  it.value()->layer()->dataProvider()->enterUpdateMode();
45  }
46  }
47 }
48 
50 {
51  qDeleteAll( mCheckErrors );
52  qDeleteAll( mChecks );
53  for ( auto it = mFeaturePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
54  {
55  if ( it.value()->layer() )
56  {
57  it.value()->layer()->dataProvider()->leaveUpdateMode();
58  it.value()->layer()->setReadOnly( false );
59  }
60  delete it.value();
61  }
62  delete mContext;
63 }
64 
65 QFuture<void> QgsGeometryChecker::execute( int *totalSteps )
66 {
67  if ( totalSteps )
68  {
69  *totalSteps = 0;
70  for ( QgsGeometryCheck *check : qgis::as_const( mChecks ) )
71  {
72  for ( auto it = mFeaturePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
73  {
74  if ( check->checkType() <= QgsGeometryCheck::FeatureCheck )
75  {
76  *totalSteps += check->isCompatible( it.value()->layer() ) ? it.value()->allFeatureIds().size() : 0;
77  }
78  else
79  {
80  *totalSteps += 1;
81  }
82  }
83  }
84  }
85 
86  QFuture<void> future = QtConcurrent::map( mChecks, RunCheckWrapper( this ) );
87 
88  QFutureWatcher<void> *watcher = new QFutureWatcher<void>();
89  watcher->setFuture( future );
90  QTimer *timer = new QTimer();
91  connect( timer, &QTimer::timeout, this, &QgsGeometryChecker::emitProgressValue );
92  connect( watcher, &QFutureWatcherBase::finished, timer, &QObject::deleteLater );
93  connect( watcher, &QFutureWatcherBase::finished, watcher, &QObject::deleteLater );
94  timer->start( 500 );
95 
96  return future;
97 }
98 
99 void QgsGeometryChecker::emitProgressValue()
100 {
101  emit progressValue( mFeedback.progress() );
102 }
103 
104 bool QgsGeometryChecker::fixError( QgsGeometryCheckError *error, int method, bool triggerRepaint )
105 {
106  mMessages.clear();
107  if ( error->status() >= QgsGeometryCheckError::StatusFixed )
108  {
109  return true;
110  }
111 #if 0
112  QTextStream( stdout ) << "Fixing " << error->description() << ": " << error->layerId() << ":" << error->featureId() << " @[" << error->vidx().part << ", " << error->vidx().ring << ", " << error->vidx().vertex << "](" << error->location().x() << ", " << error->location().y() << ") = " << error->value().toString() << endl;
113 #endif
114 
116  QgsRectangle recheckArea = error->affectedAreaBBox();
117 
118  error->check()->fixError( mFeaturePools, error, method, mMergeAttributeIndices, changes );
119 #if 0
120  QTextStream( stdout ) << " * Status: " << error->resolutionMessage() << endl;
121  static QVector<QString> strChangeWhat = { "ChangeFeature", "ChangePart", "ChangeRing", "ChangeNode" };
122  static QVector<QString> strChangeType = { "ChangeAdded", "ChangeRemoved", "ChangeChanged" };
123  for ( const QString &layerId : changes.keys() )
124  {
125  for ( const QgsFeatureId &fid : changes[layerId].keys() )
126  {
127  for ( const QgsGeometryCheck::Change &change : changes[layerId][fid] )
128  {
129  QTextStream( stdout ) << " * Change: " << layerId << ":" << fid << " :: " << strChangeWhat[change.what] << ":" << strChangeType[change.type] << ":(" << change.vidx.part << "," << change.vidx.ring << "," << change.vidx.vertex << ")" << endl;
130  }
131  }
132  }
133 #endif
134  emit errorUpdated( error, true );
135  if ( error->status() != QgsGeometryCheckError::StatusFixed )
136  {
137  return false;
138  }
139 
140  // If nothing was changed, stop here
141  if ( changes.isEmpty() )
142  {
143  return true;
144  }
145 
146  // Determine what to recheck
147  // - Collect all features which were changed, get affected area
148  QMap<QString, QSet<QgsFeatureId>> recheckFeatures;
149  for ( auto it = changes.constBegin(); it != changes.constEnd(); ++it )
150  {
151  const QMap<QgsFeatureId, QList<QgsGeometryCheck::Change>> &layerChanges = it.value();
152  QgsFeaturePool *featurePool = mFeaturePools[it.key()];
153  QgsCoordinateTransform t( featurePool->layer()->crs(), mContext->mapCrs, QgsProject::instance() );
154  for ( auto layerChangeIt = layerChanges.constBegin(); layerChangeIt != layerChanges.constEnd(); ++layerChangeIt )
155  {
156  bool removed = false;
157  for ( const QgsGeometryCheck::Change &change : layerChangeIt.value() )
158  {
159  if ( change.what == QgsGeometryCheck::ChangeFeature && change.type == QgsGeometryCheck::ChangeRemoved )
160  {
161  removed = true;
162  break;
163  }
164  }
165  if ( !removed )
166  {
167  QgsFeature f;
168  if ( featurePool->getFeature( layerChangeIt.key(), f ) )
169  {
170  recheckFeatures[it.key()].insert( layerChangeIt.key() );
172  }
173  }
174  }
175  }
176  // - Determine extent to recheck for gaps
177  for ( QgsGeometryCheckError *err : qgis::as_const( mCheckErrors ) )
178  {
179  if ( err->check()->checkType() == QgsGeometryCheck::LayerCheck )
180  {
181  if ( err->affectedAreaBBox().intersects( recheckArea ) )
182  {
183  recheckArea.combineExtentWith( err->affectedAreaBBox() );
184  }
185  }
186  }
187  recheckArea.grow( 10 * mContext->tolerance );
188  QMap<QString, QgsFeatureIds> recheckAreaFeatures;
189  for ( const QString &layerId : mFeaturePools.keys() )
190  {
191  QgsFeaturePool *featurePool = mFeaturePools[layerId];
192  QgsCoordinateTransform t( mContext->mapCrs, featurePool->layer()->crs(), QgsProject::instance() );
193  recheckAreaFeatures[layerId] = featurePool->getIntersects( t.transform( recheckArea ) );
194  }
195 
196  // Recheck feature / changed area to detect new errors
197  QList<QgsGeometryCheckError *> recheckErrors;
198  for ( const QgsGeometryCheck *check : qgis::as_const( mChecks ) )
199  {
200  if ( check->checkType() == QgsGeometryCheck::LayerCheck )
201  {
202  if ( !recheckAreaFeatures.isEmpty() )
203  {
204  check->collectErrors( mFeaturePools, recheckErrors, mMessages, nullptr, recheckAreaFeatures );
205  }
206  }
207  else
208  {
209  if ( !recheckFeatures.isEmpty() )
210  {
211  check->collectErrors( mFeaturePools, recheckErrors, mMessages, nullptr, recheckFeatures );
212  }
213  }
214  }
215 
216  // Go through error list, update other errors of the checked feature
217  for ( QgsGeometryCheckError *err : qgis::as_const( mCheckErrors ) )
218  {
219  if ( err == error || err->status() == QgsGeometryCheckError::StatusObsolete )
220  {
221  continue;
222  }
223 
224  QgsGeometryCheckError::Status oldStatus = err->status();
225 
226  bool handled = err->handleChanges( changes );
227 
228  // Check if this error now matches one found when rechecking the feature/area
229  QgsGeometryCheckError *matchErr = nullptr;
230  int nMatch = 0;
231  for ( QgsGeometryCheckError *recheckErr : qgis::as_const( recheckErrors ) )
232  {
233  if ( recheckErr->isEqual( err ) || recheckErr->closeMatch( err ) )
234  {
235  ++nMatch;
236  matchErr = recheckErr;
237  }
238  }
239  // If just one close match was found, take it
240  if ( nMatch == 1 && matchErr )
241  {
242  err->update( matchErr );
243  emit errorUpdated( err, err->status() != oldStatus );
244  recheckErrors.removeAll( matchErr );
245  delete matchErr;
246  continue;
247  }
248 
249  // If no match is found and the error is not fixed or obsolete, set it to obsolete if...
250  if ( err->status() < QgsGeometryCheckError::StatusFixed &&
251  (
252  // changes weren't handled
253  !handled ||
254  // or if it is a FeatureNodeCheck or FeatureCheck error whose feature was rechecked
255  ( err->check()->checkType() <= QgsGeometryCheck::FeatureCheck && recheckFeatures[err->layerId()].contains( err->featureId() ) ) ||
256  // or if it is a LayerCheck error within the rechecked area
257  ( err->check()->checkType() == QgsGeometryCheck::LayerCheck && recheckArea.contains( err->affectedAreaBBox() ) )
258  )
259  )
260  {
261  err->setObsolete();
262  emit errorUpdated( err, err->status() != oldStatus );
263  }
264  }
265 
266  // Add new errors
267  for ( QgsGeometryCheckError *recheckErr : qgis::as_const( recheckErrors ) )
268  {
269  emit errorAdded( recheckErr );
270  mCheckErrors.append( recheckErr );
271  }
272 
273  if ( triggerRepaint )
274  {
275  for ( const QString &layerId : changes.keys() )
276  {
277  mFeaturePools[layerId]->layer()->triggerRepaint();
278  }
279  }
280 
281  return true;
282 }
283 
284 void QgsGeometryChecker::runCheck( const QMap<QString, QgsFeaturePool *> &featurePools, const QgsGeometryCheck *check )
285 {
286  // Run checks
287  QList<QgsGeometryCheckError *> errors;
288  QStringList messages;
289  check->collectErrors( featurePools, errors, messages, &mFeedback );
290  mErrorListMutex.lock();
291  mCheckErrors.append( errors );
292  mMessages.append( messages );
293  mErrorListMutex.unlock();
294  for ( QgsGeometryCheckError *error : qgis::as_const( errors ) )
295  {
296  emit errorAdded( error );
297  }
298 }
299 
300 QgsGeometryChecker::RunCheckWrapper::RunCheckWrapper( QgsGeometryChecker *instance )
301  : mInstance( instance )
302 {
303 }
304 
305 void QgsGeometryChecker::RunCheckWrapper::operator()( const QgsGeometryCheck *check )
306 {
307  mInstance->runCheck( mInstance->mFeaturePools, check );
308 }
Class for doing transforms between two map coordinate systems.
QgsPointXY transform(const QgsPointXY &point, TransformDirection direction=ForwardTransform) const SIP_THROW(QgsCsException)
Transform the point from the source CRS to the destination CRS.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, TransformDirection direction=ForwardTransform, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
A feature pool is based on a vector layer and caches features.
QgsFeatureIds getIntersects(const QgsRectangle &rect) const
Gets all feature ids in the bounding box rect.
QgsVectorLayer * layer() const
Gets a pointer to the underlying layer.
bool getFeature(QgsFeatureId id, QgsFeature &feature)
Retrieves the feature with the specified id into feature.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:56
QgsGeometry geometry
Definition: qgsfeature.h:67
double progress() const
Returns the current progress reported by the feedback object.
Definition: qgsfeedback.h:80
Base configuration for geometry checks.
const QgsCoordinateReferenceSystem mapCrs
The coordinate system in which calculations should be done.
const double tolerance
The tolerance to allow for in geometry checks.
This represents an error reported by a geometry check.
const QString & layerId() const
The id of the layer on which this error has been detected.
Status
The status of an error.
@ StatusFixed
The error is fixed.
@ StatusObsolete
The error is obsolete because of other modifications.
Status status() const
The status of the error.
const QgsVertexId & vidx() const
The id of the affected vertex.
QString resolutionMessage() const
A message with details, how the error has been resolved.
QgsFeatureId featureId() const
The id of the feature on which this error has been detected.
virtual void update(const QgsGeometryCheckError *other)
Update this error with the information from other.
virtual QString description() const
The error description.
const QgsPointXY & location() const
The location of the error in map units.
const QgsGeometryCheck * check() const
The geometry check that created this error.
QVariant value() const
An additional value for the error.
void setObsolete()
Set the error status to obsolete.
virtual QgsRectangle affectedAreaBBox() const
The bounding box of the affected area of the error.
This class implements a geometry check.
QMap< QString, QMap< QgsFeatureId, QList< QgsGeometryCheck::Change > > > Changes
A collection of changes.
virtual void collectErrors(const QMap< QString, QgsFeaturePool * > &featurePools, QList< QgsGeometryCheckError * > &errors, QStringList &messages, QgsFeedback *feedback, const LayerFeatureIds &ids=QgsGeometryCheck::LayerFeatureIds()) const =0
The main worker method.
@ ChangeFeature
This change happens on feature level.
@ LayerCheck
The check controls a whole layer (topology checks)
@ FeatureCheck
The check controls geometries as a whole.
@ ChangeRemoved
Something has been removed.
virtual void fixError(const QMap< QString, QgsFeaturePool * > &featurePools, QgsGeometryCheckError *error, int method, const QMap< QString, int > &mergeAttributeIndices, Changes &changes) const
Fixes the error error with the specified method.
Manages and runs a set of geometry checks.
QFuture< void > execute(int *totalSteps=nullptr)
void progressValue(int value)
bool fixError(QgsGeometryCheckError *error, int method, bool triggerRepaint=false)
QgsGeometryChecker(const QList< QgsGeometryCheck * > &checks, QgsGeometryCheckContext *context, const QMap< QString, QgsFeaturePool * > &featurePools)
void errorAdded(QgsGeometryCheckError *error)
void errorUpdated(QgsGeometryCheckError *error, bool statusChanged)
const QMap< QString, QgsFeaturePool * > featurePools() const
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:91
double y
Definition: qgspointxy.h:48
Q_GADGET double x
Definition: qgspointxy.h:47
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:501
A rectangle specified with double values.
Definition: qgsrectangle.h:42
bool contains(const QgsRectangle &rect) const
Returns true when rectangle contains other rectangle.
Definition: qgsrectangle.h:342
void grow(double delta)
Grows the rectangle in place by the specified amount.
Definition: qgsrectangle.h:275
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
Definition: qgsrectangle.h:359
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
Descripts a change to fix a geometry.
int vertex
Vertex number.
int part
Part number.
int ring
Ring number.