MinimumBoundingGeometry.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. """
  2. ***************************************************************************
  3. MinimumBoundingGeometry.py
  4. --------------------------
  5. Date : September 2017
  6. Copyright : (C) 2017 by Nyall Dawson
  7. Email : nyall dot dawson at gmail 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__ = 'Nyall Dawson'
  18. __date__ = 'September 2017'
  19. __copyright__ = '(C) 2017, Nyall Dawson'
  20. import os
  21. import math
  22. from qgis.PyQt.QtGui import QIcon
  23. from qgis.PyQt.QtCore import QVariant
  24. from qgis.core import (QgsApplication,
  25. QgsField,
  26. QgsFeatureSink,
  27. QgsGeometry,
  28. QgsWkbTypes,
  29. QgsFeatureRequest,
  30. QgsFields,
  31. QgsRectangle,
  32. QgsProcessingException,
  33. QgsProcessingParameterFeatureSource,
  34. QgsProcessingParameterField,
  35. QgsProcessingParameterEnum,
  36. QgsProcessingParameterFeatureSink,
  37. QgsProcessing,
  38. QgsFeature,
  39. QgsVertexId,
  40. QgsMultiPoint)
  41. from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
  42. pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
  43. class MinimumBoundingGeometry(QgisAlgorithm):
  44. INPUT = 'INPUT'
  45. OUTPUT = 'OUTPUT'
  46. TYPE = 'TYPE'
  47. FIELD = 'FIELD'
  48. def icon(self):
  49. return QgsApplication.getThemeIcon("/algorithms/mAlgorithmConvexHull.svg")
  50. def svgIconPath(self):
  51. return QgsApplication.iconPath("/algorithms/mAlgorithmConvexHull.svg")
  52. def group(self):
  53. return self.tr('Vector geometry')
  54. def groupId(self):
  55. return 'vectorgeometry'
  56. def __init__(self):
  57. super().__init__()
  58. self.type_names = [self.tr('Envelope (Bounding Box)'),
  59. self.tr('Minimum Oriented Rectangle'),
  60. self.tr('Minimum Enclosing Circle'),
  61. self.tr('Convex Hull')]
  62. def initAlgorithm(self, config=None):
  63. self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
  64. self.tr('Input layer')))
  65. self.addParameter(QgsProcessingParameterField(self.FIELD,
  66. self.tr(
  67. 'Field (optional, set if features should be grouped by class)'),
  68. parentLayerParameterName=self.INPUT, optional=True))
  69. self.addParameter(QgsProcessingParameterEnum(self.TYPE,
  70. self.tr('Geometry type'), options=self.type_names))
  71. self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Bounding geometry'),
  72. QgsProcessing.TypeVectorPolygon))
  73. def name(self):
  74. return 'minimumboundinggeometry'
  75. def displayName(self):
  76. return self.tr('Minimum bounding geometry')
  77. def tags(self):
  78. return self.tr(
  79. 'bounding,box,bounds,envelope,minimum,oriented,rectangle,enclosing,circle,convex,hull,generalization').split(
  80. ',')
  81. def processAlgorithm(self, parameters, context, feedback):
  82. source = self.parameterAsSource(parameters, self.INPUT, context)
  83. if source is None:
  84. raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
  85. field_name = self.parameterAsString(parameters, self.FIELD, context)
  86. type = self.parameterAsEnum(parameters, self.TYPE, context)
  87. use_field = bool(field_name)
  88. field_index = -1
  89. fields = QgsFields()
  90. fields.append(QgsField('id', QVariant.Int, '', 20))
  91. if use_field:
  92. # keep original field type, name and parameters
  93. field_index = source.fields().lookupField(field_name)
  94. if field_index >= 0:
  95. fields.append(source.fields()[field_index])
  96. if type == 0:
  97. # envelope
  98. fields.append(QgsField('width', QVariant.Double, '', 20, 6))
  99. fields.append(QgsField('height', QVariant.Double, '', 20, 6))
  100. fields.append(QgsField('area', QVariant.Double, '', 20, 6))
  101. fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
  102. elif type == 1:
  103. # oriented rect
  104. fields.append(QgsField('width', QVariant.Double, '', 20, 6))
  105. fields.append(QgsField('height', QVariant.Double, '', 20, 6))
  106. fields.append(QgsField('angle', QVariant.Double, '', 20, 6))
  107. fields.append(QgsField('area', QVariant.Double, '', 20, 6))
  108. fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
  109. elif type == 2:
  110. # circle
  111. fields.append(QgsField('radius', QVariant.Double, '', 20, 6))
  112. fields.append(QgsField('area', QVariant.Double, '', 20, 6))
  113. elif type == 3:
  114. # convex hull
  115. fields.append(QgsField('area', QVariant.Double, '', 20, 6))
  116. fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
  117. (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
  118. fields, QgsWkbTypes.Polygon, source.sourceCrs())
  119. if sink is None:
  120. raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
  121. if field_index >= 0:
  122. geometry_dict = {}
  123. bounds_dict = {}
  124. total = 50.0 / source.featureCount() if source.featureCount() else 1
  125. features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([field_index]))
  126. for current, f in enumerate(features):
  127. if feedback.isCanceled():
  128. break
  129. if not f.hasGeometry():
  130. continue
  131. if type == 0:
  132. # bounding boxes - calculate on the fly for efficiency
  133. if f[field_index] not in bounds_dict:
  134. bounds_dict[f[field_index]] = f.geometry().boundingBox()
  135. else:
  136. bounds_dict[f[field_index]].combineExtentWith(f.geometry().boundingBox())
  137. else:
  138. if f[field_index] not in geometry_dict:
  139. geometry_dict[f[field_index]] = [f.geometry()]
  140. else:
  141. geometry_dict[f[field_index]].append(f.geometry())
  142. feedback.setProgress(int(current * total))
  143. # bounding boxes
  144. current = 0
  145. if type == 0:
  146. total = 50.0 / len(bounds_dict) if bounds_dict else 1
  147. for group, rect in bounds_dict.items():
  148. if feedback.isCanceled():
  149. break
  150. # envelope
  151. feature = QgsFeature()
  152. feature.setGeometry(QgsGeometry.fromRect(rect))
  153. feature.setAttributes([current, group, rect.width(), rect.height(), rect.area(), rect.perimeter()])
  154. sink.addFeature(feature, QgsFeatureSink.FastInsert)
  155. geometry_dict[group] = None
  156. feedback.setProgress(50 + int(current * total))
  157. current += 1
  158. else:
  159. total = 50.0 / len(geometry_dict) if geometry_dict else 1
  160. for group, geometries in geometry_dict.items():
  161. if feedback.isCanceled():
  162. break
  163. feature = self.createFeature(feedback, current, type, geometries, group)
  164. sink.addFeature(feature, QgsFeatureSink.FastInsert)
  165. geometry_dict[group] = None
  166. feedback.setProgress(50 + int(current * total))
  167. current += 1
  168. else:
  169. total = 80.0 / source.featureCount() if source.featureCount() else 1
  170. features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]))
  171. geometry_queue = []
  172. bounds = QgsRectangle()
  173. for current, f in enumerate(features):
  174. if feedback.isCanceled():
  175. break
  176. if not f.hasGeometry():
  177. continue
  178. if type == 0:
  179. # bounding boxes, calculate on the fly for efficiency
  180. bounds.combineExtentWith(f.geometry().boundingBox())
  181. else:
  182. geometry_queue.append(f.geometry())
  183. feedback.setProgress(int(current * total))
  184. if not feedback.isCanceled():
  185. if type == 0:
  186. feature = QgsFeature()
  187. feature.setGeometry(QgsGeometry.fromRect(bounds))
  188. feature.setAttributes([0, bounds.width(), bounds.height(), bounds.area(), bounds.perimeter()])
  189. else:
  190. feature = self.createFeature(feedback, 0, type, geometry_queue)
  191. sink.addFeature(feature, QgsFeatureSink.FastInsert)
  192. return {self.OUTPUT: dest_id}
  193. def createFeature(self, feedback, feature_id, type, geometries, class_field=None):
  194. attrs = [feature_id]
  195. if class_field is not None:
  196. attrs.append(class_field)
  197. multi_point = QgsMultiPoint()
  198. for g in geometries:
  199. if feedback.isCanceled():
  200. break
  201. vid = QgsVertexId()
  202. while True:
  203. if feedback.isCanceled():
  204. break
  205. found, point = g.constGet().nextVertex(vid)
  206. if found:
  207. multi_point.addGeometry(point)
  208. else:
  209. break
  210. geometry = QgsGeometry(multi_point)
  211. output_geometry = None
  212. if type == 0:
  213. # envelope
  214. rect = geometry.boundingBox()
  215. output_geometry = QgsGeometry.fromRect(rect)
  216. attrs.append(rect.width())
  217. attrs.append(rect.height())
  218. attrs.append(rect.area())
  219. attrs.append(rect.perimeter())
  220. elif type == 1:
  221. # oriented rect
  222. output_geometry, area, angle, width, height = geometry.orientedMinimumBoundingBox()
  223. attrs.append(width)
  224. attrs.append(height)
  225. attrs.append(angle)
  226. attrs.append(area)
  227. attrs.append(2 * width + 2 * height)
  228. elif type == 2:
  229. # circle
  230. output_geometry, center, radius = geometry.minimalEnclosingCircle(segments=72)
  231. attrs.append(radius)
  232. attrs.append(math.pi * radius * radius)
  233. elif type == 3:
  234. # convex hull
  235. output_geometry = geometry.convexHull()
  236. attrs.append(output_geometry.constGet().area())
  237. attrs.append(output_geometry.constGet().perimeter())
  238. f = QgsFeature()
  239. f.setAttributes(attrs)
  240. f.setGeometry(output_geometry)
  241. return f