Skip to content

Commit

Permalink
[FEATURE] New processing algorithm "minimum bounding geometry"
Browse files Browse the repository at this point in the history
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 83affdc
Show file tree
Hide file tree
Showing 25 changed files with 1,035 additions and 0 deletions.
7 changes: 7 additions & 0 deletions python/plugins/processing/algs/help/qgis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
237 changes: 237 additions & 0 deletions python/plugins/processing/algs/qgis/MinimumBoundingGeometry.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -254,6 +255,7 @@ def getAlgs(self):
MeanCoords(),
Merge(),
MergeLines(),
MinimumBoundingGeometry(),
MinimalEnclosingCircle(),
NearestNeighbourAnalysis(),
OffsetLine(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 83affdc

Please sign in to comment.