123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- """
- ***************************************************************************
- MinimumBoundingGeometry.py
- --------------------------
- Date : September 2017
- Copyright : (C) 2017 by Nyall Dawson
- Email : nyall dot dawson at gmail dot com
- ***************************************************************************
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- ***************************************************************************
- """
- __author__ = 'Nyall Dawson'
- __date__ = 'September 2017'
- __copyright__ = '(C) 2017, Nyall Dawson'
- import os
- import math
- from qgis.PyQt.QtGui import QIcon
- from qgis.PyQt.QtCore import QVariant
- from qgis.core import (QgsApplication,
- QgsField,
- QgsFeatureSink,
- QgsGeometry,
- QgsWkbTypes,
- QgsFeatureRequest,
- QgsFields,
- QgsRectangle,
- QgsProcessingException,
- QgsProcessingParameterFeatureSource,
- QgsProcessingParameterField,
- QgsProcessingParameterEnum,
- QgsProcessingParameterFeatureSink,
- QgsProcessing,
- QgsFeature,
- QgsVertexId,
- QgsMultiPoint)
- from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
- pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
- class MinimumBoundingGeometry(QgisAlgorithm):
- INPUT = 'INPUT'
- OUTPUT = 'OUTPUT'
- TYPE = 'TYPE'
- FIELD = 'FIELD'
- def icon(self):
- return QgsApplication.getThemeIcon("/algorithms/mAlgorithmConvexHull.svg")
- def svgIconPath(self):
- return QgsApplication.iconPath("/algorithms/mAlgorithmConvexHull.svg")
- def group(self):
- return self.tr('Vector geometry')
- def groupId(self):
- return 'vectorgeometry'
- def __init__(self):
- super().__init__()
- self.type_names = [self.tr('Envelope (Bounding Box)'),
- self.tr('Minimum Oriented Rectangle'),
- self.tr('Minimum Enclosing Circle'),
- self.tr('Convex Hull')]
- def initAlgorithm(self, config=None):
- self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
- self.tr('Input layer')))
- self.addParameter(QgsProcessingParameterField(self.FIELD,
- self.tr(
- 'Field (optional, set if features should be grouped by class)'),
- parentLayerParameterName=self.INPUT, optional=True))
- self.addParameter(QgsProcessingParameterEnum(self.TYPE,
- self.tr('Geometry type'), options=self.type_names))
- self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Bounding geometry'),
- QgsProcessing.TypeVectorPolygon))
- def name(self):
- return 'minimumboundinggeometry'
- def displayName(self):
- return self.tr('Minimum bounding geometry')
- def tags(self):
- return self.tr(
- 'bounding,box,bounds,envelope,minimum,oriented,rectangle,enclosing,circle,convex,hull,generalization').split(
- ',')
- def processAlgorithm(self, parameters, context, feedback):
- source = self.parameterAsSource(parameters, self.INPUT, context)
- if source is None:
- raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
- field_name = self.parameterAsString(parameters, self.FIELD, context)
- type = self.parameterAsEnum(parameters, self.TYPE, context)
- use_field = bool(field_name)
- field_index = -1
- fields = QgsFields()
- fields.append(QgsField('id', QVariant.Int, '', 20))
- if use_field:
- # keep original field type, name and parameters
- field_index = source.fields().lookupField(field_name)
- if field_index >= 0:
- fields.append(source.fields()[field_index])
- if type == 0:
- # envelope
- fields.append(QgsField('width', QVariant.Double, '', 20, 6))
- fields.append(QgsField('height', QVariant.Double, '', 20, 6))
- fields.append(QgsField('area', QVariant.Double, '', 20, 6))
- fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
- elif type == 1:
- # oriented rect
- fields.append(QgsField('width', QVariant.Double, '', 20, 6))
- fields.append(QgsField('height', QVariant.Double, '', 20, 6))
- fields.append(QgsField('angle', QVariant.Double, '', 20, 6))
- fields.append(QgsField('area', QVariant.Double, '', 20, 6))
- fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
- elif type == 2:
- # circle
- fields.append(QgsField('radius', QVariant.Double, '', 20, 6))
- fields.append(QgsField('area', QVariant.Double, '', 20, 6))
- elif type == 3:
- # convex hull
- fields.append(QgsField('area', QVariant.Double, '', 20, 6))
- fields.append(QgsField('perimeter', QVariant.Double, '', 20, 6))
- (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
- fields, QgsWkbTypes.Polygon, source.sourceCrs())
- if sink is None:
- raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
- if field_index >= 0:
- geometry_dict = {}
- bounds_dict = {}
- total = 50.0 / source.featureCount() if source.featureCount() else 1
- features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([field_index]))
- for current, f in enumerate(features):
- if feedback.isCanceled():
- break
- if not f.hasGeometry():
- continue
- if type == 0:
- # bounding boxes - calculate on the fly for efficiency
- if f[field_index] not in bounds_dict:
- bounds_dict[f[field_index]] = f.geometry().boundingBox()
- else:
- bounds_dict[f[field_index]].combineExtentWith(f.geometry().boundingBox())
- else:
- if f[field_index] not in geometry_dict:
- geometry_dict[f[field_index]] = [f.geometry()]
- else:
- geometry_dict[f[field_index]].append(f.geometry())
- feedback.setProgress(int(current * total))
- # bounding boxes
- current = 0
- if type == 0:
- total = 50.0 / len(bounds_dict) if bounds_dict else 1
- for group, rect in bounds_dict.items():
- if feedback.isCanceled():
- break
- # envelope
- feature = QgsFeature()
- feature.setGeometry(QgsGeometry.fromRect(rect))
- feature.setAttributes([current, group, rect.width(), rect.height(), rect.area(), rect.perimeter()])
- sink.addFeature(feature, QgsFeatureSink.FastInsert)
- geometry_dict[group] = None
- feedback.setProgress(50 + int(current * total))
- current += 1
- else:
- total = 50.0 / len(geometry_dict) if geometry_dict else 1
- for group, geometries in geometry_dict.items():
- if feedback.isCanceled():
- break
- feature = self.createFeature(feedback, current, type, geometries, group)
- sink.addFeature(feature, QgsFeatureSink.FastInsert)
- geometry_dict[group] = None
- feedback.setProgress(50 + int(current * total))
- current += 1
- else:
- total = 80.0 / source.featureCount() if source.featureCount() else 1
- features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]))
- geometry_queue = []
- bounds = QgsRectangle()
- for current, f in enumerate(features):
- if feedback.isCanceled():
- break
- if not f.hasGeometry():
- continue
- if type == 0:
- # bounding boxes, calculate on the fly for efficiency
- bounds.combineExtentWith(f.geometry().boundingBox())
- else:
- geometry_queue.append(f.geometry())
- feedback.setProgress(int(current * total))
- if not feedback.isCanceled():
- if type == 0:
- feature = QgsFeature()
- feature.setGeometry(QgsGeometry.fromRect(bounds))
- feature.setAttributes([0, bounds.width(), bounds.height(), bounds.area(), bounds.perimeter()])
- else:
- feature = self.createFeature(feedback, 0, type, geometry_queue)
- sink.addFeature(feature, QgsFeatureSink.FastInsert)
- return {self.OUTPUT: dest_id}
- def createFeature(self, feedback, feature_id, type, geometries, class_field=None):
- attrs = [feature_id]
- if class_field is not None:
- attrs.append(class_field)
- multi_point = QgsMultiPoint()
- for g in geometries:
- if feedback.isCanceled():
- break
- vid = QgsVertexId()
- while True:
- if feedback.isCanceled():
- break
- found, point = g.constGet().nextVertex(vid)
- if found:
- multi_point.addGeometry(point)
- else:
- break
- geometry = QgsGeometry(multi_point)
- output_geometry = None
- if type == 0:
- # envelope
- rect = geometry.boundingBox()
- output_geometry = QgsGeometry.fromRect(rect)
- attrs.append(rect.width())
- attrs.append(rect.height())
- attrs.append(rect.area())
- attrs.append(rect.perimeter())
- elif type == 1:
- # oriented rect
- output_geometry, area, angle, width, height = geometry.orientedMinimumBoundingBox()
- attrs.append(width)
- attrs.append(height)
- attrs.append(angle)
- attrs.append(area)
- attrs.append(2 * width + 2 * height)
- elif type == 2:
- # circle
- output_geometry, center, radius = geometry.minimalEnclosingCircle(segments=72)
- attrs.append(radius)
- attrs.append(math.pi * radius * radius)
- elif type == 3:
- # convex hull
- output_geometry = geometry.convexHull()
- attrs.append(output_geometry.constGet().area())
- attrs.append(output_geometry.constGet().perimeter())
- f = QgsFeature()
- f.setAttributes(attrs)
- f.setGeometry(output_geometry)
- return f
|