Skip to content

Commit ee601b0

Browse files
author
cfarmer
committed
[FEATURE] completely new geometry checking tool for fTools. Significantly (big time) faster, more relevant error messages, and now supports zooming to errors. Thanks to jef for the new QgsGeometry.validateGeometry function. Fixes #3169 and #2611.
git-svn-id: http://svn.osgeo.org/qgis/trunk@14606 c8812cc2-4d05-0410-92ff-de0c093fc19c
1 parent 12c7f2c commit ee601b0

File tree

3 files changed

+180
-142
lines changed

3 files changed

+180
-142
lines changed

python/plugins/fTools/fTools.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import doIntersectLines, doJoinAttributes, doSelectByLocation, doVectorSplit, doMeanCoords
4747
import doPointDistance, doPointsInPolygon, doRandom, doRandPoints, doRegPoints, doDefineProj
4848
import doReProject, doSpatialJoin, doSubsetSelect, doSumLines, doVectorGrid, doMergeShapes
49-
import doAbout
49+
import doAbout, doValidate
5050

5151
class fToolsPlugin:
5252
def __init__( self,iface ):
@@ -257,7 +257,8 @@ def dopolysToLines( self ):
257257
d.exec_()
258258

259259
def docheckGeom( self ):
260-
d = doVisual.VisualDialog( self.iface, 1 )
260+
d = doValidate.ValidateDialog(self.iface)
261+
d.show()
261262
d.exec_()
262263

263264
def domultiToSingle( self ):
+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# -*- coding: utf-8 -*-
2+
from PyQt4.QtCore import *
3+
from PyQt4.QtGui import *
4+
from qgis.core import *
5+
from ui_frmVisual import Ui_Dialog
6+
import ftools_utils
7+
import math
8+
9+
class ValidateDialog( QDialog, Ui_Dialog ):
10+
def __init__(self, iface):
11+
QDialog.__init__(self)
12+
self.iface = iface
13+
self.setupUi(self)
14+
self.setModal(False) # we want to be able to interact with the featuresmc.extent().width()
15+
# adjust user interface
16+
self.setWindowTitle( self.tr( "Check geometry validity" ) )
17+
self.cmbField.setVisible( False )
18+
self.label.setVisible( False )
19+
self.useSelected.setVisible( True )
20+
self.label_2.setText( self.tr( "Geometry errors" ) )
21+
self.label_4.setText( self.tr( "Total encountered errors" ) )
22+
self.partProgressBar.setVisible( False )
23+
self.tblUnique.setSelectionMode(QAbstractItemView.SingleSelection)
24+
self.tblUnique.setSelectionBehavior(QAbstractItemView.SelectRows)
25+
# populate list of available layers
26+
myList = ftools_utils.getLayerNames( [ QGis.Point, QGis.Line, QGis.Polygon ] )
27+
self.connect(self.tblUnique, SIGNAL("currentItemChanged(QTableWidgetItem*, QTableWidgetItem*)" ),
28+
self.zoomToError)
29+
self.inShape.addItems( myList )
30+
self.cancel_close = self.buttonBox_2.button(QDialogButtonBox.Close)
31+
self.buttonOk = self.buttonBox_2.button(QDialogButtonBox.Ok)
32+
self.progressBar.setValue(0)
33+
self.storedScale = self.iface.mapCanvas().scale()
34+
35+
def keyPressEvent( self, e ):
36+
if ( e.modifiers() == Qt.ControlModifier or \
37+
e.modifiers() == Qt.MetaModifier ) and \
38+
e.key() == Qt.Key_C:
39+
#selection = self.tblUnique.selectedItems()
40+
items = QString()
41+
for row in range( self.tblUnique.rowCount() ):
42+
items.append( self.tblUnique.item( row, 0 ).text()
43+
+ "," + self.tblUnique.item( row, 1 ).text() + "\n" )
44+
if not items.isEmpty():
45+
clip_board = QApplication.clipboard()
46+
clip_board.setText( items )
47+
else:
48+
QDialog.keyPressEvent( self, e )
49+
50+
def accept( self ):
51+
if self.inShape.currentText() == "":
52+
QMessageBox.information( self, self.tr("Error!"), self.tr( "Please specify input vector layer" ) )
53+
elif self.cmbField.isVisible() and self.cmbField.currentText() == "":
54+
QMessageBox.information( self, self.tr("Error!"), self.tr( "Please specify input field" ) )
55+
else:
56+
self.validate( self.inShape.currentText(), self.useSelected.checkState() )
57+
58+
def zoomToError(self, curr, prev):
59+
if curr is None:
60+
return
61+
row = curr.row() # if we clicked in the first column, we want the second
62+
item = self.tblUnique.item(row, 1)
63+
if not item.data(Qt.UserRole) is None:
64+
mc = self.iface.mapCanvas()
65+
x = item.data(Qt.UserRole).toPyObject().x()
66+
y = item.data(Qt.UserRole).toPyObject().y()
67+
mc.zoomToPreviousExtent()
68+
scale = mc.scale()
69+
rect = QgsRectangle(float(x)-(4.0/scale),float(y)-(4.0/scale),
70+
float(x)+(4.0/scale),float(y)+(4.0/scale))
71+
# Set the extent to our new rectangle
72+
mc.setExtent(rect)
73+
# Refresh the map
74+
mc.refresh()
75+
76+
def validate( self, myLayer, mySelection ):
77+
vlayer = ftools_utils.getVectorLayerByName( myLayer )
78+
self.tblUnique.clearContents()
79+
self.tblUnique.setRowCount( 0 )
80+
self.lstCount.clear()
81+
self.buttonOk.setEnabled( False )
82+
self.testThread = validateThread( self.iface.mainWindow(), self, vlayer, mySelection )
83+
QObject.connect( self.testThread, SIGNAL( "runFinished(PyQt_PyObject)" ), self.runFinishedFromThread )
84+
QObject.connect( self.testThread, SIGNAL( "runStatus(PyQt_PyObject)" ), self.runStatusFromThread )
85+
QObject.connect( self.testThread, SIGNAL( "runRange(PyQt_PyObject)" ), self.runRangeFromThread )
86+
self.cancel_close.setText( self.tr("Cancel") )
87+
QObject.connect( self.cancel_close, SIGNAL( "clicked()" ), self.cancelThread )
88+
QApplication.setOverrideCursor( Qt.WaitCursor )
89+
self.testThread.start()
90+
return True
91+
92+
def cancelThread( self ):
93+
self.testThread.stop()
94+
QApplication.restoreOverrideCursor()
95+
self.buttonOk.setEnabled( True )
96+
97+
def runFinishedFromThread( self, output ):
98+
self.testThread.stop()
99+
QApplication.restoreOverrideCursor()
100+
self.buttonOk.setEnabled( True )
101+
self.tblUnique.setColumnCount( 2 )
102+
count = 0
103+
for rec in output:
104+
if len(rec[1]) < 1:
105+
continue
106+
where = None
107+
for err in rec[1]: # for each error we find
108+
self.tblUnique.insertRow(count)
109+
fidItem = QTableWidgetItem( str(rec[0]) )
110+
self.tblUnique.setItem( count, 0, fidItem )
111+
if err.hasWhere(): # if there is a location associated with the error
112+
where = err.where()
113+
message = err.what()
114+
errItem = QTableWidgetItem( message )
115+
errItem.setData(Qt.UserRole, QVariant(where))
116+
self.tblUnique.setItem( count, 1, errItem )
117+
count += 1
118+
self.tblUnique.setHorizontalHeaderLabels( [ self.tr("Feature"), self.tr("Error(s)") ] )
119+
self.tblUnique.horizontalHeader().setResizeMode( 0, QHeaderView.ResizeToContents )
120+
self.tblUnique.horizontalHeader().show()
121+
self.tblUnique.horizontalHeader().setResizeMode( 1, QHeaderView.Stretch )
122+
self.tblUnique.resizeRowsToContents()
123+
self.lstCount.insert(str(count))
124+
self.cancel_close.setText( "Close" )
125+
QObject.disconnect( self.cancel_close, SIGNAL( "clicked()" ), self.cancelThread )
126+
return True
127+
128+
def runStatusFromThread( self, status ):
129+
self.progressBar.setValue( status )
130+
131+
def runRangeFromThread( self, range_vals ):
132+
self.progressBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
133+
134+
class validateThread( QThread ):
135+
def __init__( self, parentThread, parentObject, vlayer, mySelection ):
136+
QThread.__init__( self, parentThread )
137+
self.parent = parentObject
138+
self.running = False
139+
self.vlayer = vlayer
140+
self.mySelection = mySelection
141+
142+
def run( self ):
143+
self.running = True
144+
output = self.check_geometry( self.vlayer )
145+
self.emit( SIGNAL( "runFinished(PyQt_PyObject)" ), output )
146+
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )
147+
148+
def stop(self):
149+
self.running = False
150+
151+
def check_geometry( self, vlayer ):
152+
lstErrors = []
153+
if self.mySelection:
154+
layer = vlayer.selectedFeatures()
155+
nFeat = len(layer)
156+
else:
157+
layer = vlayer
158+
layer.select([]) # select all features, and ignore attributes
159+
nFeat = layer.featureCount()
160+
nElement = 0
161+
if nFeat > 0:
162+
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )
163+
self.emit( SIGNAL( "runRange(PyQt_PyObject)" ), ( 0, nFeat ) )
164+
for feat in layer:
165+
if not self.running:
166+
return list()
167+
geom = QgsGeometry(feat.geometry()) # ger reference to geometry
168+
self.emit(SIGNAL("runStatus(PyQt_PyObject)"), nElement)
169+
nElement += 1
170+
lstErrors.append((feat.id(), list(geom.validateGeometry())))
171+
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nFeat )
172+
return lstErrors

python/plugins/fTools/tools/doVisual.py

+5-140
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,7 @@ def accept( self ):
6565
self.visual( self.inShape.currentText(), self.cmbField.currentText(), self.useSelected.checkState() )
6666

6767
def manageGui( self ):
68-
if self.myFunction == 1: # Check geometry validity
69-
self.setWindowTitle( self.tr( "Check geometry validity" ) )
70-
self.cmbField.setVisible( False )
71-
self.label.setVisible( False )
72-
self.useSelected.setVisible( False )
73-
self.label_2.setText( self.tr( "Geometry errors" ) )
74-
self.label_4.setText( self.tr( "Total encountered errors" ) )
75-
elif self.myFunction == 2: # List unique values
68+
if self.myFunction == 2: # List unique values
7669
self.setWindowTitle( self.tr( "List unique values" ) )
7770
self.label_2.setText( self.tr( "Unique values" ) )
7871
self.label_4.setText(self.tr( "Total unique values" ) )
@@ -93,16 +86,14 @@ def manageGui( self ):
9386
self.lstCount.setVisible( False )
9487
self.resize( 381, 200 )
9588
self.inShape.clear()
96-
if self.myFunction == 1:
97-
myList = ftools_utils.getLayerNames( [ QGis.Polygon ] )
98-
elif self.myFunction == 4:
89+
if self.myFunction == 4:
9990
myList = ftools_utils.getLayerNames( [ QGis.Point ] )
10091
else:
10192
myList = ftools_utils.getLayerNames( [ QGis.Point, QGis.Line, QGis.Polygon ] )
10293
self.inShape.addItems( myList )
10394
return
10495

105-
#1: Check geometry
96+
#1: Check geometry (disabled)
10697
#2: List unique values
10798
#3: Basic statistics
10899
#4: Nearest neighbour analysis
@@ -192,9 +183,8 @@ def __init__( self, parentThread, parentObject, function, vlayer, myField, mySel
192183

193184
def run( self ):
194185
self.running = True
195-
if self.myFunction == 1: # Check geometry
196-
( lst, cnt ) = self.check_geometry( self.vlayer )
197-
elif self.myFunction == 2: # List unique values
186+
# note that 1 used to be associated with check_geometry
187+
if self.myFunction == 2: # List unique values
198188
( lst, cnt ) = self.list_unique_values( self.vlayer, self.myField )
199189
elif self.myFunction == 3: # Basic statistics
200190
( lst, cnt ) = self.basic_statistics( self.vlayer, self.myField )
@@ -422,128 +412,3 @@ def nearest_neighbour_analysis( self, vlayer ):
422412
lstStats.append( self.tr( "N:" ) + unicode( nVal ) )
423413
lstStats.append( self.tr( "Z-Score:" ) + unicode( zscore ) )
424414
return ( lstStats, [] )
425-
426-
def check_geometry( self, vlayer ):
427-
vprovider = vlayer.dataProvider()
428-
allAttrs = vprovider.attributeIndexes()
429-
vprovider.select( allAttrs )
430-
feat = QgsFeature()
431-
geom = QgsGeometry()
432-
count = 0
433-
lstErrors = []
434-
nFeat = vprovider.featureCount()
435-
nElement = 0
436-
if nFeat > 0:
437-
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )
438-
self.emit( SIGNAL( "runRange(PyQt_PyObject)" ), ( 0, nFeat ) )
439-
440-
while vprovider.nextFeature( feat ):
441-
geom = QgsGeometry( feat.geometry() )
442-
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nElement )
443-
nElement += 1
444-
if geom.isMultipart():
445-
polygons = geom.asMultiPolygon()
446-
for polygon in polygons:
447-
if not self.isHoleNested( polygon ):
448-
lstErrors.append( self.tr( "Feature %1 contains an unnested hole" ).arg( unicode( feat.id() ) ) )
449-
count += 1
450-
if not self.isPolygonClosed( polygon ):
451-
lstErrors.append( self.tr( "Feature %1 is not closed" ).arg( unicode( feat.id() ) ) )
452-
count += 1
453-
if self.isSelfIntersecting( polygon ):
454-
lstErrors.append( self.tr( "Feature %1 is self intersecting" ).arg( unicode( feat.id() ) ) )
455-
count += 1
456-
if not self.isCorrectOrientation( polygon ):
457-
lstErrors.append( self.tr( "Feature %1 has incorrect node ordering" ).arg( unicode( feat.id() ) ) )
458-
count += 1
459-
460-
else:
461-
geom = geom.asPolygon()
462-
if not self.isHoleNested( geom ):
463-
lstErrors.append( self.tr( "Feature %1 contains an unnested hole" ).arg( unicode( feat.id() ) ) )
464-
count += 1
465-
if not self.isPolygonClosed( geom ):
466-
lstErrors.append( self.tr( "Feature %1 is not closed" ).arg( unicode( feat.id() ) ) )
467-
count += 1
468-
if self.isSelfIntersecting( geom ):
469-
lstErrors.append( self.tr( "Feature %1 is self intersecting" ).arg( unicode( feat.id() ) ) )
470-
count += 1
471-
if not self.isCorrectOrientation( geom ):
472-
lstErrors.append( self.tr( "Feature %1 has incorrect node ordering" ).arg( unicode( feat.id() ) ) )
473-
count += 1
474-
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nFeat )
475-
return ( lstErrors, count )
476-
477-
def isHoleNested( self, polygon ):
478-
if len( polygon ) <= 1:
479-
return True
480-
else:
481-
outer = polygon[ 0 ]
482-
for i in polygon[ 1: len( polygon ) ]:
483-
if not self.arePointsInside( i, outer ):
484-
return False
485-
return True
486-
487-
def arePointsInside( self, inner, outer ):
488-
outer = QgsGeometry().fromPolygon( [ outer ] )
489-
for j in inner:
490-
if not outer.contains(j):
491-
return False
492-
return True
493-
494-
def isPolygonClosed( self, polygon ):
495-
for i in polygon:
496-
first = i[ 0 ]
497-
last = i[ len( i )-1 ]
498-
if not first == last:
499-
return False
500-
return True
501-
502-
def isSelfIntersecting( self, polygon ):
503-
cPart = 0
504-
for h in polygon:
505-
cPart += len(h)
506-
507-
self.emit( SIGNAL( "runPartRange(PyQt_PyObject)" ), ( 0, cPart ) )
508-
509-
nPart = 0
510-
for h in polygon:
511-
for i in range( 0, len(h)-1 ):
512-
self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), nPart )
513-
514-
count = 0
515-
for j in range( i+1, len(h)-1 ):
516-
if QgsGeometry().fromPolyline( [ h[ i ], h[ i + 1 ] ] ).intersects( QgsGeometry().fromPolyline( [ h[ j ], h[ j + 1 ] ] ) ):
517-
count += 1
518-
519-
if (i==0 and count>2) or (i>0 and count>1):
520-
self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), cPart )
521-
return True
522-
523-
nPart += 1
524-
525-
self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), cPart )
526-
527-
return False
528-
529-
def isCorrectOrientation( self, polygon ):
530-
outer = True
531-
for h in polygon:
532-
if outer:
533-
outer = False
534-
if not self.isClockwise( h ):
535-
return False
536-
else:
537-
if self.isClockwise(h):
538-
return False
539-
return True
540-
541-
def isClockwise( self, temp ):
542-
area = 0
543-
for pt in range( 0, len( temp ) -1 ):
544-
area += ( temp[ pt ].x() * temp[ pt + 1 ].y() - temp[ pt + 1 ].x() * temp[ pt ].y() )
545-
area = area / 2
546-
if area <= 0:
547-
return True
548-
else:
549-
return False

0 commit comments

Comments
 (0)