2727__revision__ = '$Format:%H$'
2828
2929import math
30- from qgis .core import (QgsApplication ,
31- QgsFeatureRequest ,
32- QgsFeature ,
33- QgsFeatureSink ,
30+ from qgis .core import (QgsFeatureSink ,
3431 QgsGeometry ,
3532 QgsPointXY ,
36- QgsProcessingUtils )
37- from processing .tools import dataobjects
33+ QgsSpatialIndex ,
34+ QgsRectangle ,
35+ QgsProcessing ,
36+ QgsProcessingParameterFeatureSource ,
37+ QgsProcessingParameterNumber ,
38+ QgsProcessingParameterBoolean ,
39+ QgsProcessingParameterFeatureSink )
3840from processing .algs .qgis .QgisAlgorithm import QgisAlgorithm
39- from processing .core .parameters import ParameterVector
40- from processing .core .parameters import ParameterNumber
41- from processing .core .parameters import ParameterBoolean
42- from processing .core .outputs import OutputVector
4341
4442
4543class PointsDisplacement (QgisAlgorithm ):
4644
47- INPUT_LAYER = 'INPUT_LAYER '
45+ INPUT = 'INPUT '
4846 DISTANCE = 'DISTANCE'
47+ PROXIMITY = 'PROXIMITY'
4948 HORIZONTAL = 'HORIZONTAL'
50- OUTPUT_LAYER = 'OUTPUT_LAYER '
49+ OUTPUT = 'OUTPUT '
5150
5251 def group (self ):
5352 return self .tr ('Vector geometry tools' )
@@ -56,14 +55,17 @@ def __init__(self):
5655 super ().__init__ ()
5756
5857 def initAlgorithm (self , config = None ):
59- self .addParameter (ParameterVector (self .INPUT_LAYER ,
60- self .tr ('Input layer' ), [dataobjects .TYPE_VECTOR_POINT ]))
61- self .addParameter (ParameterNumber (self .DISTANCE ,
62- self .tr ('Displacement distance' ),
63- 0.00001 , 999999999.999990 , 0.00015 ))
64- self .addParameter (ParameterBoolean (self .HORIZONTAL ,
65- self .tr ('Horizontal distribution for two point case' )))
66- self .addOutput (OutputVector (self .OUTPUT_LAYER , self .tr ('Displaced' ), datatype = [dataobjects .TYPE_VECTOR_POINT ]))
58+ self .addParameter (QgsProcessingParameterFeatureSource (self .INPUT ,
59+ self .tr ('Input layer' ), [QgsProcessing .TypeVectorPoint ]))
60+ self .addParameter (QgsProcessingParameterNumber (self .PROXIMITY ,
61+ self .tr ('Minimum distance to other points' ), type = QgsProcessingParameterNumber .Double ,
62+ minValue = 0.00001 , defaultValue = 0.00015 ))
63+ self .addParameter (QgsProcessingParameterNumber (self .DISTANCE ,
64+ self .tr ('Displacement distance' ), type = QgsProcessingParameterNumber .Double ,
65+ minValue = 0.00001 , defaultValue = 0.00015 ))
66+ self .addParameter (QgsProcessingParameterBoolean (self .HORIZONTAL ,
67+ self .tr ('Horizontal distribution for two point case' )))
68+ self .addParameter (QgsProcessingParameterFeatureSink (self .OUTPUT , self .tr ('Displaced' ), QgsProcessing .TypeVectorPoint ))
6769
6870 def name (self ):
6971 return 'pointsdisplacement'
@@ -72,64 +74,110 @@ def displayName(self):
7274 return self .tr ('Points displacement' )
7375
7476 def processAlgorithm (self , parameters , context , feedback ):
75- radius = self .getParameterValue (self .DISTANCE )
76- horizontal = self .getParameterValue (self .HORIZONTAL )
77- output = self .getOutputFromName (self .OUTPUT_LAYER )
77+ source = self .parameterAsSource (parameters , self .INPUT , context )
78+ proximity = self .parameterAsDouble (parameters , self .PROXIMITY , context )
79+ radius = self .parameterAsDouble (parameters , self .DISTANCE , context )
80+ horizontal = self .parameterAsBool (parameters , self .HORIZONTAL , context )
7881
79- layer = QgsProcessingUtils .mapLayerFromString (self .getParameterValue (self .INPUT_LAYER ), context )
82+ (sink , dest_id ) = self .parameterAsSink (parameters , self .OUTPUT , context ,
83+ source .fields (), source .wkbType (), source .sourceCrs ())
8084
81- writer = output . getVectorWriter ( layer . fields (), layer . wkbType (), layer . crs (), context )
85+ features = source . getFeatures ( )
8286
83- features = QgsProcessingUtils . getFeatures ( layer , context )
87+ total = 100.0 / source . featureCount () if source . featureCount () else 0
8488
85- total = 100.0 / layer .featureCount () if layer .featureCount () else 0
89+ def searchRect (p ):
90+ return QgsRectangle (p .x () - proximity , p .y () - proximity , p .x () + proximity , p .y () + proximity )
8691
87- duplicates = dict ()
92+ index = QgsSpatialIndex ()
93+
94+ # NOTE: this is a Python port of QgsPointDistanceRenderer::renderFeature. If refining this algorithm,
95+ # please port the changes to QgsPointDistanceRenderer::renderFeature also!
96+
97+ clustered_groups = []
98+ group_index = {}
99+ group_locations = {}
88100 for current , f in enumerate (features ):
89- wkt = f .geometry ().exportToWkt ()
90- if wkt not in duplicates :
91- duplicates [wkt ] = [f .id ()]
101+ if feedback .isCanceled ():
102+ break
103+
104+ if not f .hasGeometry ():
105+ continue
106+
107+ point = f .geometry ().asPoint ()
108+
109+ other_features_within_radius = index .intersects (searchRect (point ))
110+ if not other_features_within_radius :
111+ index .insertFeature (f )
112+ group = [f ]
113+ clustered_groups .append (group )
114+ group_index [f .id ()] = len (clustered_groups ) - 1
115+ group_locations [f .id ()] = point
92116 else :
93- duplicates [wkt ].extend ([f .id ()])
117+ # find group with closest location to this point (may be more than one within search tolerance)
118+ min_dist_feature_id = other_features_within_radius [0 ]
119+ min_dist = group_locations [min_dist_feature_id ].distance (point )
120+ for i in range (1 , len (other_features_within_radius )):
121+ candidate_id = other_features_within_radius [i ]
122+ new_dist = group_locations [candidate_id ].distance (point )
123+ if new_dist < min_dist :
124+ min_dist = new_dist
125+ min_dist_feature_id = candidate_id
126+
127+ group_index_pos = group_index [min_dist_feature_id ]
128+ group = clustered_groups [group_index_pos ]
129+
130+ # calculate new centroid of group
131+ old_center = group_locations [min_dist_feature_id ]
132+ group_locations [min_dist_feature_id ] = QgsPointXY ((old_center .x () * len (group ) + point .x ()) / (len (group ) + 1.0 ),
133+ (old_center .y () * len (group ) + point .y ()) / (len (group ) + 1.0 ))
134+ # add to a group
135+ clustered_groups [group_index_pos ].append (f )
136+ group_index [f .id ()] = group_index_pos
94137
95138 feedback .setProgress (int (current * total ))
96139
97140 current = 0
98- total = 100.0 / len (duplicates ) if duplicates else 1
141+ total = 100.0 / len (clustered_groups ) if clustered_groups else 1
99142 feedback .setProgress (0 )
100143
101144 fullPerimeter = 2 * math .pi
102145
103- for (geom , fids ) in list (duplicates .items ()):
104- count = len (fids )
146+ for group in clustered_groups :
147+ if feedback .isCanceled ():
148+ break
149+
150+ count = len (group )
105151 if count == 1 :
106- f = next (layer .getFeatures (QgsFeatureRequest ().setFilterFid (fids [0 ])))
107- writer .addFeature (f , QgsFeatureSink .FastInsert )
152+ sink .addFeature (group [0 ], QgsFeatureSink .FastInsert )
108153 else :
109154 angleStep = fullPerimeter / count
110155 if count == 2 and horizontal :
111156 currentAngle = math .pi / 2
112157 else :
113158 currentAngle = 0
114159
115- old_point = QgsGeometry .fromWkt (geom ).asPoint ()
160+ old_point = group_locations [group [0 ].id ()]
161+
162+ for f in group :
163+ if feedback .isCanceled ():
164+ break
116165
117- request = QgsFeatureRequest ().setFilterFids (fids ).setFlags (QgsFeatureRequest .NoGeometry )
118- for f in layer .getFeatures (request ):
119166 sinusCurrentAngle = math .sin (currentAngle )
120167 cosinusCurrentAngle = math .cos (currentAngle )
121168 dx = radius * sinusCurrentAngle
122169 dy = radius * cosinusCurrentAngle
123170
124- new_point = QgsPointXY (old_point .x () + dx , old_point .y () + dy )
125- out_feature = QgsFeature ()
126- out_feature .setGeometry (QgsGeometry .fromPoint (new_point ))
127- out_feature .setAttributes (f .attributes ())
171+ # we want to keep any existing m/z values
172+ point = f .geometry ().geometry ().clone ()
173+ point .setX (old_point .x () + dx )
174+ point .setY (old_point .y () + dy )
175+ f .setGeometry (QgsGeometry (point ))
128176
129- writer .addFeature (out_feature , QgsFeatureSink .FastInsert )
177+ sink .addFeature (f , QgsFeatureSink .FastInsert )
130178 currentAngle += angleStep
131179
132180 current += 1
133181 feedback .setProgress (int (current * total ))
134182
135- del writer
183+ return { self . OUTPUT : dest_id }
0 commit comments