Skip to content

Commit

Permalink
[FEATURE] completely new geometry checking tool for fTools. Significa…
Browse files Browse the repository at this point in the history
…ntly (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
  • Loading branch information
cfarmer committed Nov 13, 2010
1 parent 12c7f2c commit ee601b0
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 142 deletions.
5 changes: 3 additions & 2 deletions python/plugins/fTools/fTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import doIntersectLines, doJoinAttributes, doSelectByLocation, doVectorSplit, doMeanCoords
import doPointDistance, doPointsInPolygon, doRandom, doRandPoints, doRegPoints, doDefineProj
import doReProject, doSpatialJoin, doSubsetSelect, doSumLines, doVectorGrid, doMergeShapes
import doAbout
import doAbout, doValidate

class fToolsPlugin:
def __init__( self,iface ):
Expand Down Expand Up @@ -257,7 +257,8 @@ def dopolysToLines( self ):
d.exec_()

def docheckGeom( self ):
d = doVisual.VisualDialog( self.iface, 1 )
d = doValidate.ValidateDialog(self.iface)
d.show()
d.exec_()

def domultiToSingle( self ):
Expand Down
172 changes: 172 additions & 0 deletions python/plugins/fTools/tools/doValidate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from ui_frmVisual import Ui_Dialog
import ftools_utils
import math

class ValidateDialog( QDialog, Ui_Dialog ):
def __init__(self, iface):
QDialog.__init__(self)
self.iface = iface
self.setupUi(self)
self.setModal(False) # we want to be able to interact with the featuresmc.extent().width()
# adjust user interface
self.setWindowTitle( self.tr( "Check geometry validity" ) )
self.cmbField.setVisible( False )
self.label.setVisible( False )
self.useSelected.setVisible( True )
self.label_2.setText( self.tr( "Geometry errors" ) )
self.label_4.setText( self.tr( "Total encountered errors" ) )
self.partProgressBar.setVisible( False )
self.tblUnique.setSelectionMode(QAbstractItemView.SingleSelection)
self.tblUnique.setSelectionBehavior(QAbstractItemView.SelectRows)
# populate list of available layers
myList = ftools_utils.getLayerNames( [ QGis.Point, QGis.Line, QGis.Polygon ] )
self.connect(self.tblUnique, SIGNAL("currentItemChanged(QTableWidgetItem*, QTableWidgetItem*)" ),
self.zoomToError)
self.inShape.addItems( myList )
self.cancel_close = self.buttonBox_2.button(QDialogButtonBox.Close)
self.buttonOk = self.buttonBox_2.button(QDialogButtonBox.Ok)
self.progressBar.setValue(0)
self.storedScale = self.iface.mapCanvas().scale()

def keyPressEvent( self, e ):
if ( e.modifiers() == Qt.ControlModifier or \
e.modifiers() == Qt.MetaModifier ) and \
e.key() == Qt.Key_C:
#selection = self.tblUnique.selectedItems()
items = QString()
for row in range( self.tblUnique.rowCount() ):
items.append( self.tblUnique.item( row, 0 ).text()
+ "," + self.tblUnique.item( row, 1 ).text() + "\n" )
if not items.isEmpty():
clip_board = QApplication.clipboard()
clip_board.setText( items )
else:
QDialog.keyPressEvent( self, e )

def accept( self ):
if self.inShape.currentText() == "":
QMessageBox.information( self, self.tr("Error!"), self.tr( "Please specify input vector layer" ) )
elif self.cmbField.isVisible() and self.cmbField.currentText() == "":
QMessageBox.information( self, self.tr("Error!"), self.tr( "Please specify input field" ) )
else:
self.validate( self.inShape.currentText(), self.useSelected.checkState() )

def zoomToError(self, curr, prev):
if curr is None:
return
row = curr.row() # if we clicked in the first column, we want the second
item = self.tblUnique.item(row, 1)
if not item.data(Qt.UserRole) is None:
mc = self.iface.mapCanvas()
x = item.data(Qt.UserRole).toPyObject().x()
y = item.data(Qt.UserRole).toPyObject().y()
mc.zoomToPreviousExtent()
scale = mc.scale()
rect = QgsRectangle(float(x)-(4.0/scale),float(y)-(4.0/scale),
float(x)+(4.0/scale),float(y)+(4.0/scale))
# Set the extent to our new rectangle
mc.setExtent(rect)
# Refresh the map
mc.refresh()

def validate( self, myLayer, mySelection ):
vlayer = ftools_utils.getVectorLayerByName( myLayer )
self.tblUnique.clearContents()
self.tblUnique.setRowCount( 0 )
self.lstCount.clear()
self.buttonOk.setEnabled( False )
self.testThread = validateThread( self.iface.mainWindow(), self, vlayer, mySelection )
QObject.connect( self.testThread, SIGNAL( "runFinished(PyQt_PyObject)" ), self.runFinishedFromThread )
QObject.connect( self.testThread, SIGNAL( "runStatus(PyQt_PyObject)" ), self.runStatusFromThread )
QObject.connect( self.testThread, SIGNAL( "runRange(PyQt_PyObject)" ), self.runRangeFromThread )
self.cancel_close.setText( self.tr("Cancel") )
QObject.connect( self.cancel_close, SIGNAL( "clicked()" ), self.cancelThread )
QApplication.setOverrideCursor( Qt.WaitCursor )
self.testThread.start()
return True

def cancelThread( self ):
self.testThread.stop()
QApplication.restoreOverrideCursor()
self.buttonOk.setEnabled( True )

def runFinishedFromThread( self, output ):
self.testThread.stop()
QApplication.restoreOverrideCursor()
self.buttonOk.setEnabled( True )
self.tblUnique.setColumnCount( 2 )
count = 0
for rec in output:
if len(rec[1]) < 1:
continue
where = None
for err in rec[1]: # for each error we find
self.tblUnique.insertRow(count)
fidItem = QTableWidgetItem( str(rec[0]) )
self.tblUnique.setItem( count, 0, fidItem )
if err.hasWhere(): # if there is a location associated with the error
where = err.where()
message = err.what()
errItem = QTableWidgetItem( message )
errItem.setData(Qt.UserRole, QVariant(where))
self.tblUnique.setItem( count, 1, errItem )
count += 1
self.tblUnique.setHorizontalHeaderLabels( [ self.tr("Feature"), self.tr("Error(s)") ] )
self.tblUnique.horizontalHeader().setResizeMode( 0, QHeaderView.ResizeToContents )
self.tblUnique.horizontalHeader().show()
self.tblUnique.horizontalHeader().setResizeMode( 1, QHeaderView.Stretch )
self.tblUnique.resizeRowsToContents()
self.lstCount.insert(str(count))
self.cancel_close.setText( "Close" )
QObject.disconnect( self.cancel_close, SIGNAL( "clicked()" ), self.cancelThread )
return True

def runStatusFromThread( self, status ):
self.progressBar.setValue( status )

def runRangeFromThread( self, range_vals ):
self.progressBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )

class validateThread( QThread ):
def __init__( self, parentThread, parentObject, vlayer, mySelection ):
QThread.__init__( self, parentThread )
self.parent = parentObject
self.running = False
self.vlayer = vlayer
self.mySelection = mySelection

def run( self ):
self.running = True
output = self.check_geometry( self.vlayer )
self.emit( SIGNAL( "runFinished(PyQt_PyObject)" ), output )
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )

def stop(self):
self.running = False

def check_geometry( self, vlayer ):
lstErrors = []
if self.mySelection:
layer = vlayer.selectedFeatures()
nFeat = len(layer)
else:
layer = vlayer
layer.select([]) # select all features, and ignore attributes
nFeat = layer.featureCount()
nElement = 0
if nFeat > 0:
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )
self.emit( SIGNAL( "runRange(PyQt_PyObject)" ), ( 0, nFeat ) )
for feat in layer:
if not self.running:
return list()
geom = QgsGeometry(feat.geometry()) # ger reference to geometry
self.emit(SIGNAL("runStatus(PyQt_PyObject)"), nElement)
nElement += 1
lstErrors.append((feat.id(), list(geom.validateGeometry())))
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nFeat )
return lstErrors
145 changes: 5 additions & 140 deletions python/plugins/fTools/tools/doVisual.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,7 @@ def accept( self ):
self.visual( self.inShape.currentText(), self.cmbField.currentText(), self.useSelected.checkState() )

def manageGui( self ):
if self.myFunction == 1: # Check geometry validity
self.setWindowTitle( self.tr( "Check geometry validity" ) )
self.cmbField.setVisible( False )
self.label.setVisible( False )
self.useSelected.setVisible( False )
self.label_2.setText( self.tr( "Geometry errors" ) )
self.label_4.setText( self.tr( "Total encountered errors" ) )
elif self.myFunction == 2: # List unique values
if self.myFunction == 2: # List unique values
self.setWindowTitle( self.tr( "List unique values" ) )
self.label_2.setText( self.tr( "Unique values" ) )
self.label_4.setText(self.tr( "Total unique values" ) )
Expand All @@ -93,16 +86,14 @@ def manageGui( self ):
self.lstCount.setVisible( False )
self.resize( 381, 200 )
self.inShape.clear()
if self.myFunction == 1:
myList = ftools_utils.getLayerNames( [ QGis.Polygon ] )
elif self.myFunction == 4:
if self.myFunction == 4:
myList = ftools_utils.getLayerNames( [ QGis.Point ] )
else:
myList = ftools_utils.getLayerNames( [ QGis.Point, QGis.Line, QGis.Polygon ] )
self.inShape.addItems( myList )
return

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

def run( self ):
self.running = True
if self.myFunction == 1: # Check geometry
( lst, cnt ) = self.check_geometry( self.vlayer )
elif self.myFunction == 2: # List unique values
# note that 1 used to be associated with check_geometry
if self.myFunction == 2: # List unique values
( lst, cnt ) = self.list_unique_values( self.vlayer, self.myField )
elif self.myFunction == 3: # Basic statistics
( lst, cnt ) = self.basic_statistics( self.vlayer, self.myField )
Expand Down Expand Up @@ -422,128 +412,3 @@ def nearest_neighbour_analysis( self, vlayer ):
lstStats.append( self.tr( "N:" ) + unicode( nVal ) )
lstStats.append( self.tr( "Z-Score:" ) + unicode( zscore ) )
return ( lstStats, [] )

def check_geometry( self, vlayer ):
vprovider = vlayer.dataProvider()
allAttrs = vprovider.attributeIndexes()
vprovider.select( allAttrs )
feat = QgsFeature()
geom = QgsGeometry()
count = 0
lstErrors = []
nFeat = vprovider.featureCount()
nElement = 0
if nFeat > 0:
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), 0 )
self.emit( SIGNAL( "runRange(PyQt_PyObject)" ), ( 0, nFeat ) )

while vprovider.nextFeature( feat ):
geom = QgsGeometry( feat.geometry() )
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nElement )
nElement += 1
if geom.isMultipart():
polygons = geom.asMultiPolygon()
for polygon in polygons:
if not self.isHoleNested( polygon ):
lstErrors.append( self.tr( "Feature %1 contains an unnested hole" ).arg( unicode( feat.id() ) ) )
count += 1
if not self.isPolygonClosed( polygon ):
lstErrors.append( self.tr( "Feature %1 is not closed" ).arg( unicode( feat.id() ) ) )
count += 1
if self.isSelfIntersecting( polygon ):
lstErrors.append( self.tr( "Feature %1 is self intersecting" ).arg( unicode( feat.id() ) ) )
count += 1
if not self.isCorrectOrientation( polygon ):
lstErrors.append( self.tr( "Feature %1 has incorrect node ordering" ).arg( unicode( feat.id() ) ) )
count += 1

else:
geom = geom.asPolygon()
if not self.isHoleNested( geom ):
lstErrors.append( self.tr( "Feature %1 contains an unnested hole" ).arg( unicode( feat.id() ) ) )
count += 1
if not self.isPolygonClosed( geom ):
lstErrors.append( self.tr( "Feature %1 is not closed" ).arg( unicode( feat.id() ) ) )
count += 1
if self.isSelfIntersecting( geom ):
lstErrors.append( self.tr( "Feature %1 is self intersecting" ).arg( unicode( feat.id() ) ) )
count += 1
if not self.isCorrectOrientation( geom ):
lstErrors.append( self.tr( "Feature %1 has incorrect node ordering" ).arg( unicode( feat.id() ) ) )
count += 1
self.emit( SIGNAL( "runStatus(PyQt_PyObject)" ), nFeat )
return ( lstErrors, count )

def isHoleNested( self, polygon ):
if len( polygon ) <= 1:
return True
else:
outer = polygon[ 0 ]
for i in polygon[ 1: len( polygon ) ]:
if not self.arePointsInside( i, outer ):
return False
return True

def arePointsInside( self, inner, outer ):
outer = QgsGeometry().fromPolygon( [ outer ] )
for j in inner:
if not outer.contains(j):
return False
return True

def isPolygonClosed( self, polygon ):
for i in polygon:
first = i[ 0 ]
last = i[ len( i )-1 ]
if not first == last:
return False
return True

def isSelfIntersecting( self, polygon ):
cPart = 0
for h in polygon:
cPart += len(h)

self.emit( SIGNAL( "runPartRange(PyQt_PyObject)" ), ( 0, cPart ) )

nPart = 0
for h in polygon:
for i in range( 0, len(h)-1 ):
self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), nPart )

count = 0
for j in range( i+1, len(h)-1 ):
if QgsGeometry().fromPolyline( [ h[ i ], h[ i + 1 ] ] ).intersects( QgsGeometry().fromPolyline( [ h[ j ], h[ j + 1 ] ] ) ):
count += 1

if (i==0 and count>2) or (i>0 and count>1):
self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), cPart )
return True

nPart += 1

self.emit( SIGNAL( "runPartStatus(PyQt_PyObject)" ), cPart )

return False

def isCorrectOrientation( self, polygon ):
outer = True
for h in polygon:
if outer:
outer = False
if not self.isClockwise( h ):
return False
else:
if self.isClockwise(h):
return False
return True

def isClockwise( self, temp ):
area = 0
for pt in range( 0, len( temp ) -1 ):
area += ( temp[ pt ].x() * temp[ pt + 1 ].y() - temp[ pt + 1 ].x() * temp[ pt ].y() )
area = area / 2
if area <= 0:
return True
else:
return False

0 comments on commit ee601b0

Please sign in to comment.