CheckValidity.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. """
  2. ***************************************************************************
  3. CheckValidity.py
  4. ---------------------
  5. Date : May 2015
  6. Copyright : (C) 2015 by Arnaud Morvan
  7. Email : arnaud dot morvan at camptocamp dot com
  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. __author__ = 'Arnaud Morvan'
  18. __date__ = 'May 2015'
  19. __copyright__ = '(C) 2015, Arnaud Morvan'
  20. import os
  21. from qgis.PyQt.QtGui import QIcon
  22. from qgis.PyQt.QtCore import QVariant
  23. from qgis.core import (Qgis,
  24. QgsApplication,
  25. QgsSettings,
  26. QgsGeometry,
  27. QgsFeature,
  28. QgsField,
  29. QgsFeatureRequest,
  30. QgsFeatureSink,
  31. QgsWkbTypes,
  32. QgsFields,
  33. QgsProcessing,
  34. QgsProcessingException,
  35. QgsProcessingFeatureSource,
  36. QgsProcessingParameterFeatureSource,
  37. QgsProcessingParameterEnum,
  38. QgsProcessingParameterFeatureSink,
  39. QgsProcessingOutputNumber,
  40. QgsProcessingParameterBoolean)
  41. from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
  42. settings_method_key = "/digitizing/validate-geometries"
  43. pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
  44. class CheckValidity(QgisAlgorithm):
  45. INPUT_LAYER = 'INPUT_LAYER'
  46. METHOD = 'METHOD'
  47. VALID_OUTPUT = 'VALID_OUTPUT'
  48. VALID_COUNT = 'VALID_COUNT'
  49. INVALID_OUTPUT = 'INVALID_OUTPUT'
  50. INVALID_COUNT = 'INVALID_COUNT'
  51. ERROR_OUTPUT = 'ERROR_OUTPUT'
  52. ERROR_COUNT = 'ERROR_COUNT'
  53. IGNORE_RING_SELF_INTERSECTION = 'IGNORE_RING_SELF_INTERSECTION'
  54. def icon(self):
  55. return QgsApplication.getThemeIcon("/algorithms/mAlgorithmCheckGeometry.svg")
  56. def svgIconPath(self):
  57. return QgsApplication.iconPath("/algorithms/mAlgorithmCheckGeometry.svg")
  58. def group(self):
  59. return self.tr('Vector geometry')
  60. def groupId(self):
  61. return 'vectorgeometry'
  62. def tags(self):
  63. return self.tr('valid,invalid,detect,error').split(',')
  64. def __init__(self):
  65. super().__init__()
  66. def initAlgorithm(self, config=None):
  67. self.methods = [self.tr('The one selected in digitizing settings'),
  68. 'QGIS',
  69. 'GEOS']
  70. self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT_LAYER,
  71. self.tr('Input layer')))
  72. self.addParameter(QgsProcessingParameterEnum(self.METHOD,
  73. self.tr('Method'), self.methods, defaultValue=2))
  74. self.parameterDefinition(self.METHOD).setMetadata({
  75. 'widget_wrapper': {
  76. 'useCheckBoxes': True,
  77. 'columns': 3}})
  78. self.addParameter(QgsProcessingParameterBoolean(self.IGNORE_RING_SELF_INTERSECTION,
  79. self.tr('Ignore ring self intersections'), defaultValue=False))
  80. self.addParameter(QgsProcessingParameterFeatureSink(self.VALID_OUTPUT, self.tr('Valid output'), QgsProcessing.TypeVectorAnyGeometry, None, True))
  81. self.addOutput(QgsProcessingOutputNumber(self.VALID_COUNT, self.tr('Count of valid features')))
  82. self.addParameter(QgsProcessingParameterFeatureSink(self.INVALID_OUTPUT, self.tr('Invalid output'), QgsProcessing.TypeVectorAnyGeometry, None, True))
  83. self.addOutput(QgsProcessingOutputNumber(self.INVALID_COUNT, self.tr('Count of invalid features')))
  84. self.addParameter(QgsProcessingParameterFeatureSink(self.ERROR_OUTPUT, self.tr('Error output'), QgsProcessing.TypeVectorAnyGeometry, None, True))
  85. self.addOutput(QgsProcessingOutputNumber(self.ERROR_COUNT, self.tr('Count of errors')))
  86. def name(self):
  87. return 'checkvalidity'
  88. def displayName(self):
  89. return self.tr('Check validity')
  90. def processAlgorithm(self, parameters, context, feedback):
  91. ignore_ring_self_intersection = self.parameterAsBoolean(parameters, self.IGNORE_RING_SELF_INTERSECTION, context)
  92. method_param = self.parameterAsEnum(parameters, self.METHOD, context)
  93. if method_param == 0:
  94. settings = QgsSettings()
  95. method = int(settings.value(settings_method_key, 0)) - 1
  96. method = max(method, 0)
  97. else:
  98. method = method_param - 1
  99. return self.doCheck(
  100. method, parameters, context, feedback, ignore_ring_self_intersection
  101. )
  102. def doCheck(self, method, parameters, context, feedback, ignore_ring_self_intersection):
  103. flags = QgsGeometry.FlagAllowSelfTouchingHoles if ignore_ring_self_intersection else QgsGeometry.ValidityFlags()
  104. source = self.parameterAsSource(parameters, self.INPUT_LAYER, context)
  105. if source is None:
  106. raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT_LAYER))
  107. (valid_output_sink, valid_output_dest_id) = self.parameterAsSink(parameters, self.VALID_OUTPUT, context,
  108. source.fields(), source.wkbType(), source.sourceCrs())
  109. valid_count = 0
  110. invalid_fields = source.fields()
  111. invalid_fields.append(QgsField('_errors', QVariant.String, 'string', 255))
  112. (invalid_output_sink, invalid_output_dest_id) = self.parameterAsSink(parameters, self.INVALID_OUTPUT, context,
  113. invalid_fields, source.wkbType(), source.sourceCrs())
  114. invalid_count = 0
  115. error_fields = QgsFields()
  116. error_fields.append(QgsField('message', QVariant.String, 'string', 255))
  117. (error_output_sink, error_output_dest_id) = self.parameterAsSink(parameters, self.ERROR_OUTPUT, context,
  118. error_fields, QgsWkbTypes.Point, source.sourceCrs())
  119. error_count = 0
  120. features = source.getFeatures(QgsFeatureRequest(), QgsProcessingFeatureSource.FlagSkipGeometryValidityChecks)
  121. total = 100.0 / source.featureCount() if source.featureCount() else 0
  122. for current, inFeat in enumerate(features):
  123. if feedback.isCanceled():
  124. break
  125. geom = inFeat.geometry()
  126. attrs = inFeat.attributes()
  127. valid = True
  128. if not geom.isNull() and not geom.isEmpty():
  129. errors = list(geom.validateGeometry(Qgis.GeometryValidationEngine(method), flags))
  130. if errors:
  131. valid = False
  132. reasons = []
  133. for error in errors:
  134. errFeat = QgsFeature()
  135. error_geom = QgsGeometry.fromPointXY(error.where())
  136. errFeat.setGeometry(error_geom)
  137. errFeat.setAttributes([error.what()])
  138. if error_output_sink:
  139. error_output_sink.addFeature(errFeat, QgsFeatureSink.FastInsert)
  140. error_count += 1
  141. reasons.append(error.what())
  142. reason = "\n".join(reasons)
  143. if len(reason) > 255:
  144. reason = reason[:252] + '…'
  145. attrs.append(reason)
  146. outFeat = QgsFeature()
  147. outFeat.setGeometry(geom)
  148. outFeat.setAttributes(attrs)
  149. if valid:
  150. if valid_output_sink:
  151. valid_output_sink.addFeature(outFeat, QgsFeatureSink.FastInsert)
  152. valid_count += 1
  153. else:
  154. if invalid_output_sink:
  155. invalid_output_sink.addFeature(outFeat, QgsFeatureSink.FastInsert)
  156. invalid_count += 1
  157. feedback.setProgress(int(current * total))
  158. results = {
  159. self.VALID_COUNT: valid_count,
  160. self.INVALID_COUNT: invalid_count,
  161. self.ERROR_COUNT: error_count
  162. }
  163. if valid_output_sink:
  164. results[self.VALID_OUTPUT] = valid_output_dest_id
  165. if invalid_output_sink:
  166. results[self.INVALID_OUTPUT] = invalid_output_dest_id
  167. if error_output_sink:
  168. results[self.ERROR_OUTPUT] = error_output_dest_id
  169. return results