# -*- 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. * * * *************************************************************************** """ __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 (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.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', 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 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 not f.attributes()[field_index] in bounds_dict: bounds_dict[f.attributes()[field_index]] = f.geometry().boundingBox() else: bounds_dict[f.attributes()[field_index]].combineExtentWith(f.geometry().boundingBox()) else: 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)) if type == 0: # bounding boxes current = 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.FastInsert) geometry_dict[group] = None feedback.setProgress(50 + int(current * total)) current += 1 else: 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 = [] 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.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