1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    EliminateSelection.py
6    ---------------------
7    Date                 : January 2017
8    Copyright         : (C) 2017 by Bernhard Ströbl
9    Email                : bernhard.stroebl@jena.de
10***************************************************************************
11*                                                                         *
12*   This program is free software; you can redistribute it and/or modify  *
13*   it under the terms of the GNU General Public License as published by  *
14*   the Free Software Foundation; either version 2 of the License, or     *
15*   (at your option) any later version.                                   *
16*                                                                         *
17***************************************************************************
18"""
19
20__author__ = 'Bernhard Ströbl'
21__date__ = 'January 2017'
22__copyright__ = '(C) 2017, Bernhard Ströbl'
23
24import os
25
26from qgis.PyQt.QtGui import QIcon
27
28from qgis.core import (QgsApplication,
29                       QgsFeatureRequest,
30                       QgsFeature,
31                       QgsFeatureSink,
32                       QgsGeometry,
33                       QgsProcessingAlgorithm,
34                       QgsProcessingException,
35                       QgsProcessingUtils,
36                       QgsProcessingParameterVectorLayer,
37                       QgsProcessingParameterEnum,
38                       QgsProcessing,
39                       QgsProcessingParameterFeatureSink)
40
41from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
42
43pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
44
45
46class EliminateSelection(QgisAlgorithm):
47    INPUT = 'INPUT'
48    OUTPUT = 'OUTPUT'
49    MODE = 'MODE'
50
51    MODE_LARGEST_AREA = 0
52    MODE_SMALLEST_AREA = 1
53    MODE_BOUNDARY = 2
54
55    def icon(self):
56        return QgsApplication.getThemeIcon("/algorithms/mAlgorithmDissolve.svg")
57
58    def svgIconPath(self):
59        return QgsApplication.iconPath("/algorithms/mAlgorithmDissolve.svg")
60
61    def group(self):
62        return self.tr('Vector geometry')
63
64    def groupId(self):
65        return 'vectorgeometry'
66
67    def __init__(self):
68        super().__init__()
69
70    def flags(self):
71        return super().flags() | QgsProcessingAlgorithm.FlagNoThreading | QgsProcessingAlgorithm.FlagNotAvailableInStandaloneTool
72
73    def initAlgorithm(self, config=None):
74        self.modes = [self.tr('Largest Area'),
75                      self.tr('Smallest Area'),
76                      self.tr('Largest Common Boundary')]
77
78        self.addParameter(QgsProcessingParameterVectorLayer(self.INPUT,
79                                                            self.tr('Input layer'), [QgsProcessing.TypeVectorPolygon]))
80        self.addParameter(QgsProcessingParameterEnum(self.MODE,
81                                                     self.tr('Merge selection with the neighbouring polygon with the'),
82                                                     options=self.modes))
83
84        self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Eliminated'), QgsProcessing.TypeVectorPolygon))
85
86    def name(self):
87        return 'eliminateselectedpolygons'
88
89    def displayName(self):
90        return self.tr('Eliminate selected polygons')
91
92    def processAlgorithm(self, parameters, context, feedback):
93        inLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
94        boundary = self.parameterAsEnum(parameters, self.MODE, context) == self.MODE_BOUNDARY
95        smallestArea = self.parameterAsEnum(parameters, self.MODE, context) == self.MODE_SMALLEST_AREA
96
97        if inLayer.selectedFeatureCount() == 0:
98            feedback.reportError(self.tr('{0}: (No selection in input layer "{1}")').format(self.displayName(), parameters[self.INPUT]))
99
100        featToEliminate = []
101        selFeatIds = inLayer.selectedFeatureIds()
102
103        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
104                                               inLayer.fields(), inLayer.wkbType(), inLayer.sourceCrs())
105        if sink is None:
106            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
107
108        for aFeat in inLayer.getFeatures():
109            if feedback.isCanceled():
110                break
111
112            if aFeat.id() in selFeatIds:
113                # Keep references to the features to eliminate
114                featToEliminate.append(aFeat)
115            else:
116                # write the others to output
117                sink.addFeature(aFeat, QgsFeatureSink.FastInsert)
118
119        del sink
120
121        # Delete all features to eliminate in processLayer
122        processLayer = QgsProcessingUtils.mapLayerFromString(dest_id, context)
123        processLayer.startEditing()
124
125        # ANALYZE
126        if len(featToEliminate) > 0:  # Prevent zero division
127            start = 20.00
128            add = 80.00 / len(featToEliminate)
129        else:
130            start = 100
131
132        feedback.setProgress(start)
133        madeProgress = True
134
135        # We go through the list and see if we find any polygons we can
136        # merge the selected with. If we have no success with some we
137        # merge and then restart the whole story.
138        while madeProgress:  # Check if we made any progress
139            madeProgress = False
140            featNotEliminated = []
141
142            # Iterate over the polygons to eliminate
143            for i in range(len(featToEliminate)):
144                if feedback.isCanceled():
145                    break
146
147                feat = featToEliminate.pop()
148                geom2Eliminate = feat.geometry()
149                bbox = geom2Eliminate.boundingBox()
150                fit = processLayer.getFeatures(
151                    QgsFeatureRequest().setFilterRect(bbox).setSubsetOfAttributes([]))
152                mergeWithFid = None
153                mergeWithGeom = None
154                max = 0
155                min = -1
156                selFeat = QgsFeature()
157
158                # use prepared geometries for faster intersection tests
159                engine = QgsGeometry.createGeometryEngine(geom2Eliminate.constGet())
160                engine.prepareGeometry()
161
162                while fit.nextFeature(selFeat):
163                    if feedback.isCanceled():
164                        break
165
166                    selGeom = selFeat.geometry()
167
168                    if engine.intersects(selGeom.constGet()):
169                        # We have a candidate
170                        iGeom = geom2Eliminate.intersection(selGeom)
171
172                        if not iGeom:
173                            continue
174
175                        if boundary:
176                            selValue = iGeom.length()
177                        else:
178                            # area. We need a common boundary in
179                            # order to merge
180                            if 0 < iGeom.length():
181                                selValue = selGeom.area()
182                            else:
183                                selValue = -1
184
185                        if -1 != selValue:
186                            useThis = True
187
188                            if smallestArea:
189                                if -1 == min:
190                                    min = selValue
191                                else:
192                                    if selValue < min:
193                                        min = selValue
194                                    else:
195                                        useThis = False
196                            else:
197                                if selValue > max:
198                                    max = selValue
199                                else:
200                                    useThis = False
201
202                            if useThis:
203                                mergeWithFid = selFeat.id()
204                                mergeWithGeom = QgsGeometry(selGeom)
205                # End while fit
206
207                if mergeWithFid is not None:
208                    # A successful candidate
209                    newGeom = mergeWithGeom.combine(geom2Eliminate)
210
211                    if processLayer.changeGeometry(mergeWithFid, newGeom):
212                        madeProgress = True
213                    else:
214                        raise QgsProcessingException(
215                            self.tr('Could not replace geometry of feature with id {0}').format(mergeWithFid))
216
217                    start = start + add
218                    feedback.setProgress(start)
219                else:
220                    featNotEliminated.append(feat)
221
222            # End for featToEliminate
223
224            featToEliminate = featNotEliminated
225
226        # End while
227        if not processLayer.commitChanges():
228            raise QgsProcessingException(self.tr('Could not commit changes'))
229
230        for feature in featNotEliminated:
231            if feedback.isCanceled():
232                break
233
234            processLayer.dataProvider().addFeature(feature, QgsFeatureSink.FastInsert)
235
236        return {self.OUTPUT: dest_id}
237