QGIS/python/plugins/processing/algs/qgis/MinimumBoundingGeometry.py
Nyall Dawson 83affdc7f5 [FEATURE] New processing algorithm "minimum bounding geometry"
This algorithm creates geometries which enclose the features
from an input layer.

Numerous enclosing geometry types are supported, including
bounding boxes (envelopes), oriented rectangles, circles and
convex hulls.

Optionally, the features can be grouped by a field. If set,
this causes the output layer to contain one feature per grouped
value with a minimal geometry covering just the features with
matching values.
2017-09-04 08:42:03 +10:00

238 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
"""
***************************************************************************
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. *
* *
***************************************************************************
"""
from builtins import str
__author__ = 'Nyall Dawson'
__date__ = 'September 2017'
__copyright__ = '(C) 2017, Nyall Dawson'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import os
import math
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtCore import QVariant
from qgis.core import (QgsField,
QgsFeatureSink,
QgsGeometry,
QgsWkbTypes,
QgsFeatureRequest,
QgsFields,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterField,
QgsProcessingParameterEnum,
QgsProcessingParameterFeatureSink,
QgsProcessing,
QgsFeature,
QgsVertexId,
QgsMultiPointV2)
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 QIcon(os.path.join(pluginPath, 'images', 'ftools', 'convex_hull.png'))
def group(self):
return self.tr('Vector geometry')
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)
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 field_index >= 0:
geometry_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 not f.attributes()[field_index] in geometry_dict:
geometry_dict[f.attributes()[field_index]] = [f.geometry()]
else:
geometry_dict[f.attributes()[field_index]].append(f.geometry())
feedback.setProgress(int(current * total))
current = 0
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 = []
for current, f in enumerate(features):
if feedback.isCanceled():
break
if not f.hasGeometry():
continue
geometry_queue.append(f.geometry())
feedback.setProgress(int(current * total))
if not feedback.isCanceled():
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 = QgsMultiPointV2()
for g in geometries:
if feedback.isCanceled():
break
vid = QgsVertexId()
while True:
if feedback.isCanceled():
break
found, point = g.geometry().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.geometry().area())
attrs.append(output_geometry.geometry().perimeter())
f = QgsFeature()
f.setAttributes(attrs)
f.setGeometry(output_geometry)
return f