QGIS/python/plugins/processing/algs/qgis/MinimumBoundingGeometry.py
2024-11-29 15:38:02 +01:00

337 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)
sink.finalize()
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