17
17
***************************************************************************
18
18
"""
19
19
20
- __author__ = 'Victor Olaya'
20
+ __authors__ = 'Victor Olaya, Håvard Tveite '
21
21
__date__ = 'August 2012'
22
22
__copyright__ = '(C) 2012, Victor Olaya'
23
23
@@ -71,15 +71,18 @@ def __init__(self):
71
71
super ().__init__ ()
72
72
73
73
def initAlgorithm (self , config = None ):
74
- self .addParameter (QgsProcessingParameterFeatureSource (
75
- self .INPUT , self .tr ('Input layer' ), [QgsProcessing .TypeVectorPoint ]))
74
+ self .addParameter (
75
+ QgsProcessingParameterFeatureSource (
76
+ self .INPUT , self .tr ('Input layer' ),
77
+ [QgsProcessing .TypeVectorPoint ]))
76
78
self .addParameter (
77
79
QgsProcessingParameterNumber (
78
80
self .BUFFER , self .tr ('Buffer region (% of extent)' ),
79
81
minValue = 0.0 , maxValue = 9999999999 , defaultValue = 0.0 ))
80
-
81
- self .addParameter (QgsProcessingParameterFeatureSink (
82
- self .OUTPUT , self .tr ('Voronoi polygons' ), type = QgsProcessing .TypeVectorPolygon ))
82
+ self .addParameter (
83
+ QgsProcessingParameterFeatureSink (
84
+ self .OUTPUT , self .tr ('Voronoi polygons' ),
85
+ type = QgsProcessing .TypeVectorPolygon ))
83
86
84
87
def name (self ):
85
88
return 'voronoipolygons'
@@ -94,12 +97,13 @@ def processAlgorithm(self, parameters, context, feedback):
94
97
self .invalidSourceError (parameters , self .INPUT ))
95
98
96
99
buf = self .parameterAsDouble (parameters , self .BUFFER , context )
97
- (sink , dest_id ) = self .parameterAsSink (parameters , self .OUTPUT , context ,
98
- source .fields (), QgsWkbTypes .Polygon , source .sourceCrs ())
100
+ (sink , dest_id ) = self .parameterAsSink (parameters , self .OUTPUT ,
101
+ context , source .fields (),
102
+ QgsWkbTypes .Polygon ,
103
+ source .sourceCrs ())
99
104
if sink is None :
100
105
raise QgsProcessingException (
101
106
self .invalidSinkError (parameters , self .OUTPUT ))
102
-
103
107
outFeat = QgsFeature ()
104
108
extent = source .sourceExtent ()
105
109
extraX = extent .width () * (buf / 100.0 )
@@ -115,7 +119,11 @@ def processAlgorithm(self, parameters, context, feedback):
115
119
pts = []
116
120
ptDict = {}
117
121
ptNdx = - 1
118
-
122
+ # Find the minimum and maximum x and y for the input points
123
+ xmin = width
124
+ xmax = 0
125
+ ymin = height
126
+ ymax = 0
119
127
features = source .getFeatures ()
120
128
total = 100.0 / source .featureCount () if source .featureCount () else 0
121
129
for current , inFeat in enumerate (features ):
@@ -128,93 +136,131 @@ def processAlgorithm(self, parameters, context, feedback):
128
136
pts .append ((x , y ))
129
137
ptNdx += 1
130
138
ptDict [ptNdx ] = inFeat .id ()
139
+ if x < xmin :
140
+ xmin = x
141
+ if y < ymin :
142
+ ymin = y
143
+ if x > xmax :
144
+ xmax = x
145
+ if y > ymax :
146
+ ymax = y
131
147
feedback .setProgress (int (current * total ))
132
-
148
+ if xmin == xmax or ymin == ymax :
149
+ raise QgsProcessingException ('The extent of the input points is '
150
+ 'not a polygon (all the points are '
151
+ 'on a vertical or horizontal line) '
152
+ '- cannot make a Voronoi diagram!' )
153
+ xyminmax = [xmin , ymin , xmax , ymax ]
133
154
if len (pts ) < 3 :
134
155
raise QgsProcessingException (
135
156
self .tr ('Input file should contain at least 3 points. Choose '
136
157
'another file and try again.' ))
137
-
158
+ # Eliminate duplicate points
138
159
uniqueSet = set (item for item in pts )
139
160
ids = [pts .index (item ) for item in uniqueSet ]
140
- sl = voronoi .SiteList ([voronoi .Site (i [0 ], i [1 ], sitenum = j ) for ( j ,
141
- i ) in enumerate (uniqueSet )])
161
+ sl = voronoi .SiteList ([voronoi .Site (i [0 ], i [1 ], sitenum = j )
162
+ for ( j , i ) in enumerate (uniqueSet )])
142
163
voronoi .voronoi (sl , c )
143
- inFeat = QgsFeature ()
144
-
145
- current = 0
146
164
if len (c .polygons ) == 0 :
147
165
raise QgsProcessingException (
148
166
self .tr ('There were no polygons created.' ))
149
167
168
+ inFeat = QgsFeature ()
169
+ current = 0
150
170
total = 100.0 / len (c .polygons )
151
-
171
+ # Clip each of the generated "polygons"
152
172
for (site , edges ) in list (c .polygons .items ()):
153
173
if feedback .isCanceled ():
154
174
break
155
-
156
175
request = QgsFeatureRequest ().setFilterFid (ptDict [ids [site ]])
157
176
inFeat = next (source .getFeatures (request ))
158
- lines = self .clip_voronoi (edges , c , width , height , extent )
159
-
160
- geom = QgsGeometry .fromMultiPointXY (lines )
161
- geom = QgsGeometry (geom .convexHull ())
177
+ boundarypoints = self .clip_voronoi (edges , c , width ,
178
+ height , extent ,
179
+ inFeat .geometry ().asPoint (),
180
+ xyminmax )
181
+ ptgeom = QgsGeometry .fromMultiPointXY (boundarypoints )
182
+ geom = QgsGeometry (ptgeom .convexHull ())
162
183
outFeat .setGeometry (geom )
163
184
outFeat .setAttributes (inFeat .attributes ())
164
185
sink .addFeature (outFeat , QgsFeatureSink .FastInsert )
165
-
166
186
current += 1
167
187
feedback .setProgress (int (current * total ))
168
-
169
188
return {self .OUTPUT : dest_id }
170
189
171
- def clip_voronoi (self , edges , c , width , height , extent ):
190
+ def clip_voronoi (self , edges , c , width , height , extent , point , xyminmax ):
172
191
"""Clip voronoi function based on code written for Inkscape.
173
192
Copyright (C) 2010 Alvin Penner, penner@vaxxine.com
193
+ Clips one Thiessen polygon (convex polygon) to extent
174
194
"""
175
195
196
+ pt_x = point .x () - extent .xMinimum ()
197
+ pt_y = point .y () - extent .yMinimum ()
198
+ (xmin , ymin , xmax , ymax ) = xyminmax
199
+
176
200
def clip_line (x1 , y1 , x2 , y2 , w , h ):
177
201
if x1 < 0 and x2 < 0 :
202
+ # Completely to the left
178
203
return [0 , 0 , 0 , 0 ]
179
204
if x1 > w and x2 > w :
205
+ # Completely to the right
206
+ return [0 , 0 , 0 , 0 ]
207
+ if y1 < 0 and y2 < 0 :
208
+ # Completely below
209
+ return [0 , 0 , 0 , 0 ]
210
+ if y1 > h and y2 > h :
211
+ # Completely above
180
212
return [0 , 0 , 0 , 0 ]
181
213
if x1 < 0 :
214
+ # First point to the left
182
215
y1 = (y1 * x2 - y2 * x1 ) / (x2 - x1 )
183
216
x1 = 0
184
217
if x2 < 0 :
218
+ # Last point to the left
185
219
y2 = (y1 * x2 - y2 * x1 ) / (x2 - x1 )
186
220
x2 = 0
187
221
if x1 > w :
222
+ # First point to the right
188
223
y1 = y1 + (w - x1 ) * (y2 - y1 ) / (x2 - x1 )
189
224
x1 = w
190
225
if x2 > w :
226
+ # Last point to the right
191
227
y2 = y1 + (w - x1 ) * (y2 - y1 ) / (x2 - x1 )
192
228
x2 = w
193
- if y1 < 0 and y2 < 0 :
194
- return [0 , 0 , 0 , 0 ]
195
- if y1 > h and y2 > h :
196
- return [0 , 0 , 0 , 0 ]
197
229
if x1 == x2 and y1 == y2 :
198
230
return [0 , 0 , 0 , 0 ]
199
231
if y1 < 0 :
232
+ # First point below
200
233
x1 = (x1 * y2 - x2 * y1 ) / (y2 - y1 )
201
234
y1 = 0
202
235
if y2 < 0 :
236
+ # Second point below
203
237
x2 = (x1 * y2 - x2 * y1 ) / (y2 - y1 )
204
238
y2 = 0
205
239
if y1 > h :
240
+ # First point above
206
241
x1 = x1 + (h - y1 ) * (x2 - x1 ) / (y2 - y1 )
207
242
y1 = h
208
243
if y2 > h :
244
+ # Second point above
209
245
x2 = x1 + (h - y1 ) * (x2 - x1 ) / (y2 - y1 )
210
246
y2 = h
211
247
return [x1 , y1 , x2 , y2 ]
212
248
213
- lines = []
249
+ bndpoints = []
214
250
hasXMin = False
215
251
hasYMin = False
216
252
hasXMax = False
217
253
hasYMax = False
254
+ XMinNumber = 0
255
+ XMaxNumber = 0
256
+ YMinNumber = 0
257
+ YMaxNumber = 0
258
+ # The same line may appear twice for collinear input points,
259
+ # so have to remember which lines have contributed
260
+ XMinLine = - 1
261
+ XMaxLine = - 1
262
+ YMinLine = - 1
263
+ YMaxLine = - 1
218
264
for edge in edges :
219
265
if edge [1 ] >= 0 and edge [2 ] >= 0 :
220
266
# Two vertices
@@ -227,15 +273,18 @@ def clip_line(x1, y1, x2, y2, w, h):
227
273
height
228
274
)
229
275
elif edge [1 ] >= 0 :
230
- # Only one vertex
276
+ # Only one (left) vertex
231
277
if c .lines [edge [0 ]][1 ] == 0 :
232
278
# Vertical line
233
- xtemp = c .lines [edge [0 ]][2 ] / c .lines [edge [0 ]][0 ]
234
- if c .vertices [edge [1 ]][1 ] > height / 2 :
235
- ytemp = height
236
- else :
237
- ytemp = 0
279
+ #xtemp = c.lines[edge[0]][2] / c.lines[edge[0]][0]
280
+ xtemp = c .vertices [edge [1 ]][0 ]
281
+ ytemp = 0 - 1
282
+ #if c.vertices[edge[1]][1] > height / 2:
283
+ # ytemp = height
284
+ #else:
285
+ # ytemp = 0
238
286
else :
287
+ # Create an end of the line at the right edge - OK
239
288
xtemp = width
240
289
ytemp = (c .lines [edge [0 ]][2 ] - width *
241
290
c .lines [edge [0 ]][0 ]) / c .lines [edge [0 ]][1 ]
@@ -248,15 +297,18 @@ def clip_line(x1, y1, x2, y2, w, h):
248
297
height
249
298
)
250
299
elif edge [2 ] >= 0 :
251
- # Only one vertex
300
+ # Only one (right) vertex
252
301
if c .lines [edge [0 ]][1 ] == 0 :
253
302
# Vertical line
254
- xtemp = c .lines [edge [0 ]][2 ] / c .lines [edge [0 ]][0 ]
255
- if c .vertices [edge [2 ]][1 ] > height / 2 :
256
- ytemp = height
257
- else :
258
- ytemp = 0.0
303
+ #xtemp = c.lines[edge[0]][2] / c.lines[edge[0]][0]
304
+ xtemp = c .vertices [edge [2 ]][0 ]
305
+ ytemp = height + 1
306
+ #if c.vertices[edge[2]][1] > height / 2:
307
+ # ytemp = height
308
+ #else:
309
+ # ytemp = 0.0
259
310
else :
311
+ # End the line at the left edge - OK
260
312
xtemp = 0.0
261
313
ytemp = c .lines [edge [0 ]][2 ] / c .lines [edge [0 ]][1 ]
262
314
[x1 , y1 , x2 , y2 ] = clip_line (
@@ -267,31 +319,153 @@ def clip_line(x1, y1, x2, y2, w, h):
267
319
width ,
268
320
height ,
269
321
)
322
+ else :
323
+ # No vertex, only a line
324
+ if c .lines [edge [0 ]][1 ] == 0 :
325
+ # Vertical line - should not happen
326
+ xtemp = c .lines [edge [0 ]][2 ] / c .lines [edge [0 ]][0 ]
327
+ ytemp = 0.0
328
+ xend = xtemp
329
+ yend = height
330
+ else :
331
+ # End the line at both edges - ???
332
+ xtemp = 0.0
333
+ ytemp = c .lines [edge [0 ]][2 ] / c .lines [edge [0 ]][1 ]
334
+ xend = width
335
+ yend = (c .lines [edge [0 ]][2 ] - width *
336
+ c .lines [edge [0 ]][0 ]) / c .lines [edge [0 ]][1 ]
337
+ [x1 , y1 , x2 , y2 ] = clip_line (
338
+ xtemp ,
339
+ ytemp ,
340
+ xend ,
341
+ yend ,
342
+ width ,
343
+ height ,
344
+ )
270
345
if x1 or x2 or y1 or y2 :
271
- lines .append (QgsPointXY (x1 + extent .xMinimum (),
272
- y1 + extent .yMinimum ()))
273
- lines .append (QgsPointXY (x2 + extent .xMinimum (),
274
- y2 + extent .yMinimum ()))
346
+ bndpoints .append (QgsPointXY (x1 + extent .xMinimum (),
347
+ y1 + extent .yMinimum ()))
348
+ bndpoints .append (QgsPointXY (x2 + extent .xMinimum (),
349
+ y2 + extent .yMinimum ()))
275
350
if 0 in (x1 , x2 ):
276
351
hasXMin = True
352
+ if XMinLine != edge [0 ]:
353
+ XMinNumber = XMinNumber + 1
354
+ XMinLine = edge [0 ]
277
355
if 0 in (y1 , y2 ):
278
356
hasYMin = True
357
+ if YMinLine != edge [0 ]:
358
+ YMinNumber = YMinNumber + 1
359
+ YMinLine = edge [0 ]
279
360
if height in (y1 , y2 ):
280
361
hasYMax = True
362
+ if YMaxLine != edge [0 ]:
363
+ YMaxNumber = YMaxNumber + 1
364
+ YMaxLine = edge [0 ]
281
365
if width in (x1 , x2 ):
282
366
hasXMax = True
283
- if hasXMin :
284
- if hasYMax :
285
- lines .append (QgsPointXY (extent .xMinimum (),
286
- height + extent .yMinimum ()))
287
- if hasYMin :
288
- lines .append (QgsPointXY (extent .xMinimum (),
367
+ if XMaxLine != edge [0 ]:
368
+ XMaxNumber = XMaxNumber + 1
369
+ XMaxLine = edge [0 ]
370
+
371
+ # Add auxiliary points for corner cases, if necessary (duplicate
372
+ # points is not a problem - will be ignored later).
373
+ # a) Extreme input points (lowest, leftmost, rightmost, highest)
374
+ # A point can be extreme on both axis
375
+ if pt_x == xmin : # leftmost point
376
+ if XMinNumber == 0 :
377
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
378
+ extent .yMinimum ()))
379
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
380
+ height + extent .yMinimum ()))
381
+ elif XMinNumber == 1 :
382
+ if hasYMin :
383
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
384
+ extent .yMinimum ()))
385
+ elif hasYMax :
386
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
387
+ height + extent .yMinimum ()))
388
+ elif pt_x == xmax : # rightmost point
389
+ if XMaxNumber == 0 :
390
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
391
+ extent .yMinimum ()))
392
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
393
+ height + extent .yMinimum ()))
394
+ elif XMaxNumber == 1 :
395
+ if hasYMin :
396
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
397
+ extent .yMinimum ()))
398
+ elif HasYMax :
399
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
400
+ height + extent .yMinimum ()))
401
+ if pt_y == ymin : # lowest point
402
+ if YMinNumber == 0 :
403
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
404
+ extent .yMinimum ()))
405
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
406
+ extent .yMinimum ()))
407
+ elif YMinNumber == 1 :
408
+ if hasXMin :
409
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
410
+ extent .yMinimum ()))
411
+ elif hasXMax :
412
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
413
+ extent .yMinimum ()))
414
+ elif pt_y == ymax : # highest point
415
+ if YMaxNumber == 0 :
416
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
417
+ height + extent .yMinimum ()))
418
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
419
+ height + extent .yMinimum ()))
420
+ elif YMaxNumber == 1 :
421
+ if hasXMin :
422
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
423
+ height + extent .yMinimum ()))
424
+ elif hasXMax :
425
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
426
+ height + extent .yMinimum ()))
427
+ # b) Polygon that covers the x or the y extent:
428
+ if hasYMin and hasYMax :
429
+ if YMaxNumber > 1 :
430
+ if hasXMin :
431
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
432
+ extent .yMinimum ()))
433
+ elif hasXMax :
434
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
435
+ extent .yMinimum ()))
436
+ elif YMinNumber > 1 :
437
+ if hasXMin :
438
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
439
+ height + extent .yMinimum ()))
440
+ elif hasXMax :
441
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
442
+ height + extent .yMinimum ()))
443
+ elif hasXMin and hasXMax :
444
+ if XMaxNumber > 1 :
445
+ if hasYMin :
446
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
447
+ extent .yMinimum ()))
448
+ elif hasYMax :
449
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
450
+ extent .yMinimum ()))
451
+ elif XMinNumber > 1 :
452
+ if hasYMin :
453
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
454
+ height + extent .yMinimum ()))
455
+ elif hasYMax :
456
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
457
+ height + extent .yMinimum ()))
458
+ # c) Simple corners:
459
+ if XMinNumber == 1 and YMinNumber == 1 and not hasXMax and not hasYMax :
460
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
289
461
extent .yMinimum ()))
290
- if hasXMax :
291
- if hasYMax :
292
- lines .append (QgsPointXY (width + extent .xMinimum (),
462
+ if XMinNumber == 1 and YMaxNumber == 1 and not hasXMax and not hasYMin :
463
+ bndpoints .append (QgsPointXY (extent .xMinimum (),
293
464
height + extent .yMinimum ()))
294
- if hasYMin :
295
- lines .append (QgsPointXY (width + extent .xMinimum (),
465
+ if XMaxNumber == 1 and YMinNumber == 1 and not hasXMin and not hasYMax :
466
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
296
467
extent .yMinimum ()))
297
- return lines
468
+ if XMaxNumber == 1 and YMaxNumber == 1 and not hasXMin and not hasYMin :
469
+ bndpoints .append (QgsPointXY (width + extent .xMinimum (),
470
+ height + extent .yMinimum ()))
471
+ return bndpoints
0 commit comments