mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-19 00:07:15 -04:00
337 lines
12 KiB
Python
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
|