QGIS/python/plugins/processing/algs/qgis/MinimumBoundingGeometry.py
Andrea Giudiceandrea 81fb58f997 [processing] Fix deprecated QgsField constructor
Using QMetaType.Type in place of QVariant types for:
- "Check validity" qgis:checkvalidity
- "Climb along line" qgis:climbalongline
- "Add geometry attributes" qgis:exportaddgeometrycolumns
- "Advanced Python field calculator" qgis:advancedpythonfieldcalculator
- "Find projection" qgis:findprojection
- "Distance to nearest hub (line to hub)" qgis:distancetonearesthublinetohub
- "Distance to nearest hub (points)"  qgis:distancetonearesthubpoints
- "Concave hull (k-nearest neighbor)" qgis:knearestconcavehull
- "Minimum bounding geometry" qgis:minimumboundinggeometry
- "Distance matrix" qgis:distancematrix
- "Generate points (pixel centroids) along line" qgis:generatepointspixelcentroidsalongline
- "Random points along line" qgis:randompointsalongline
- "Random points in layer bounds" qgis:randompointsinlayerbounds
- "Random points inside polygons" qgis:randompointsinsidepolygons
- "Regular points" qgis:regularpoints
- "Statistics by categories" qgis:statisticsbycategories
- "Text to float" qgis:texttofloat
- "Topological coloring" qgis:topologicalcoloring
2024-09-16 08:48:01 +10:00

284 lines
12 KiB
Python

"""
***************************************************************************
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 QMetaType
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.SourceType.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', QMetaType.Type.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', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('height', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('area', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('perimeter', QMetaType.Type.Double, '', 20, 6))
elif type == 1:
# oriented rect
fields.append(QgsField('width', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('height', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('angle', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('area', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('perimeter', QMetaType.Type.Double, '', 20, 6))
elif type == 2:
# circle
fields.append(QgsField('radius', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('area', QMetaType.Type.Double, '', 20, 6))
elif type == 3:
# convex hull
fields.append(QgsField('area', QMetaType.Type.Double, '', 20, 6))
fields.append(QgsField('perimeter', QMetaType.Type.Double, '', 20, 6))
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, QgsWkbTypes.Type.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.Flag.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.Flag.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.Flag.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