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