Skip to content
Permalink
Browse files

[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.
  • Loading branch information
nyalldawson committed Sep 3, 2017
1 parent 85e6a63 commit 83affdc7f531a77cdb963fa8285fdc7af9015c76
Showing with 1,035 additions and 0 deletions.
  1. +7 −0 python/plugins/processing/algs/help/qgis.yaml
  2. +237 −0 python/plugins/processing/algs/qgis/MinimumBoundingGeometry.py
  3. +2 −0 python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
  4. +37 −0 python/plugins/processing/tests/testdata/expected/mbg_circle_field.gfs
  5. +50 −0 python/plugins/processing/tests/testdata/expected/mbg_circle_field.gml
  6. +31 −0 python/plugins/processing/tests/testdata/expected/mbg_circle_nofield.gfs
  7. +22 −0 python/plugins/processing/tests/testdata/expected/mbg_circle_nofield.gml
  8. +47 −0 python/plugins/processing/tests/testdata/expected/mbg_env_field.gfs
  9. +58 −0 python/plugins/processing/tests/testdata/expected/mbg_env_field.gml
  10. BIN python/plugins/processing/tests/testdata/expected/mbg_env_nofield.dbf
  11. +41 −0 python/plugins/processing/tests/testdata/expected/mbg_env_nofield.gfs
  12. +24 −0 python/plugins/processing/tests/testdata/expected/mbg_env_nofield.gml
  13. +1 −0 python/plugins/processing/tests/testdata/expected/mbg_env_nofield.prj
  14. +1 −0 python/plugins/processing/tests/testdata/expected/mbg_env_nofield.qpj
  15. BIN python/plugins/processing/tests/testdata/expected/mbg_env_nofield.shp
  16. BIN python/plugins/processing/tests/testdata/expected/mbg_env_nofield.shx
  17. +37 −0 python/plugins/processing/tests/testdata/expected/mbg_hull_field.gfs
  18. +50 −0 python/plugins/processing/tests/testdata/expected/mbg_hull_field.gml
  19. +31 −0 python/plugins/processing/tests/testdata/expected/mbg_hull_nofield.gfs
  20. +22 −0 python/plugins/processing/tests/testdata/expected/mbg_hull_nofield.gml
  21. +52 −0 python/plugins/processing/tests/testdata/expected/mbg_rect_field.gfs
  22. +62 −0 python/plugins/processing/tests/testdata/expected/mbg_rect_field.gml
  23. +46 −0 python/plugins/processing/tests/testdata/expected/mbg_rect_nofield.gfs
  24. +25 −0 python/plugins/processing/tests/testdata/expected/mbg_rect_nofield.gml
  25. +152 −0 python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -324,6 +324,13 @@ qgis:mergevectorlayers: >

The layers will all be reprojected to match the coordinate reference system of the first input layer.

qgis:minimumboundinggeometry: >
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.

qgis:multiparttosingleparts: >
This algorithm takes a vector layer with multipart geometries and generates a new one in which all geometries contain a single part. Features with multipart geometries are divided in as many different features as parts the geometry contain, and the same attributes are used for each of them.

@@ -0,0 +1,237 @@
# -*- 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
@@ -100,6 +100,7 @@
from .MeanCoords import MeanCoords
from .Merge import Merge
from .MergeLines import MergeLines
from .MinimumBoundingGeometry import MinimumBoundingGeometry
from .MinimalEnclosingCircle import MinimalEnclosingCircle
from .NearestNeighbourAnalysis import NearestNeighbourAnalysis
from .OffsetLine import OffsetLine
@@ -254,6 +255,7 @@ def getAlgs(self):
MeanCoords(),
Merge(),
MergeLines(),
MinimumBoundingGeometry(),
MinimalEnclosingCircle(),
NearestNeighbourAnalysis(),
OffsetLine(),
@@ -0,0 +1,37 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>mbg_circle_field</Name>
<ElementPath>mbg_circle_field</ElementPath>
<!--POLYGON-->
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>4</FeatureCount>
<ExtentXMin>-2.10952</ExtentXMin>
<ExtentXMax>9.37006</ExtentXMax>
<ExtentYMin>-4.60952</ExtentYMin>
<ExtentYMax>7.10328</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>id</Name>
<ElementPath>id</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>name</Name>
<ElementPath>name</ElementPath>
<Type>String</Type>
<Width>2</Width>
</PropertyDefn>
<PropertyDefn>
<Name>radius</Name>
<ElementPath>radius</ElementPath>
<Type>Real</Type>
</PropertyDefn>
<PropertyDefn>
<Name>area</Name>
<ElementPath>area</ElementPath>
<Type>Real</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>

0 comments on commit 83affdc

Please sign in to comment.
You can’t perform that action at this time.