forked from havatv/qgislinedirectionhistogramplugin
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathlinedirectionhistogram_engine.py
More file actions
316 lines (300 loc) · 14.4 KB
/
linedirectionhistogram_engine.py
File metadata and controls
316 lines (300 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# -*- coding: utf-8 -*-
from math import sqrt
# from PyQt4 import QtCore
# from PyQt4.QtCore import QCoreApplication
from qgis.PyQt import QtCore
from qgis.PyQt.QtCore import QCoreApplication
# from qgis.PyQt.QtCore import QVariant
from qgis.core import QgsWkbTypes
from qgis.core import QgsVectorLayer
from qgis.core import QgsGeometry, QgsPolygon
# Angles:
# Real world angles are measured clockwise from the 12 o'clock
# position (north)
# QGIS azimuth angles: clockwise in degrees, starting from north
# (between -180 and 180)
# QT angles are measured counter clockwise from the 3 o'clock
# position (in radians)
# The first bin starts at north + offsetangle (clockwise)
# The width depends on the directionneutral flag and the
# number of bins
class Worker(QtCore.QObject):
'''The worker that does the heavy lifting.
The number and length of the line segments of the inputlayer
line or polygon vector layer is calculated for each angle bin.
A list of bins is returned. Each bin contains the total
length and the number of line segments in the bin.
'''
# Define the signals used to communicate
progress = QtCore.pyqtSignal(int) # For reporting progress
status = QtCore.pyqtSignal(str)
error = QtCore.pyqtSignal(str)
# Signal for sending over the result:
finished = QtCore.pyqtSignal(bool, object)
def __init__(self, inputvectorlayer, bins, directionneutral,
offsetangle, selectedfeaturesonly,
tilelayer=None):
"""Initialise.
Arguments:
inputvectorlayer -- (QgsVectorLayer) The base vector
layer for the join
bins -- (int) bins for end point matching
directionneutral -- (boolean) should lines in oposite
directions be handled as having the
same directionneutral
offsetangle -- (float) Start the bins at a different
angle
selectedfeaturesonly -- (boolean) should only selected
features be considered
tilelayer -- (QgsVectorLayer) The (polygon) tile layer
"""
QtCore.QObject.__init__(self) # Essential!
# Creating instance variables from parameters
self.inputvectorlayer = inputvectorlayer
self.bins = int(bins)
self.directionneutral = directionneutral
self.offsetangle = offsetangle
self.selectedfeaturesonly = selectedfeaturesonly
self.tilelayer = tilelayer
self.binsize = 360.0 / bins
if self.directionneutral:
self.binsize = 180.0 / bins
# Creating instance variables for the progress bar ++
# Number of elements that have been processed - updated by
# calculate_progress
self.processed = 0
# Current percentage of progress - updated by
# calculate_progress
self.percentage = 0
# Flag set by kill(), checked in the loop
self.abort = False
# Number of features in the input layer - used by
# calculate_progress
self.feature_count = self.inputvectorlayer.featureCount()
# The number of elements that is needed to increment the
# progressbar - set early in run()
self.increment = self.feature_count // 1000
def run(self):
try:
inputlayer = self.inputvectorlayer
if inputlayer is None:
self.error.emit(self.tr('No input layer defined'))
self.finished.emit(False, None)
return
# Get and check the geometry type of the input layer
geometryType = self.inputvectorlayer.geometryType()
if not (geometryType == QgsWkbTypes.LineGeometry or
geometryType == QgsWkbTypes.PolygonGeometry):
self.error.emit('Only line and polygon layers are supported!')
self.finished.emit(False, None)
return
self.processed = 0
self.percentage = 0
if self.selectedfeaturesonly:
self.feature_count = inputlayer.selectedFeatureCount()
else:
self.feature_count = inputlayer.featureCount()
if self.feature_count == 0:
self.error.emit("No features in layer")
self.finished.emit(False, None)
return
self.increment = self.feature_count // 1000
# Initialise the result list
statistics = []
# Initialise the bins for the over all result
mybins = []
for i in range(self.bins):
mybins.append([0.0, 0])
# Add the over all bins
statistics.append(mybins)
# Get the features (iterator)
if self.selectedfeaturesonly:
features = inputlayer.getSelectedFeatures()
# features = inputlayer.selectedFeaturesIterator()
else:
features = inputlayer.getFeatures()
# Create a list for the (possible) tile (Polygon)
# geometries
tilegeoms = []
if self.tilelayer is not None:
self.status.emit("Using tiles!")
for tilefeat in self.tilelayer.getFeatures():
tilegeoms.append(tilefeat.geometry())
# Initialise and add bins for all the tiles
for i in range(len(tilegeoms)):
mybins = []
for j in range(self.bins):
mybins.append([0.0, 0])
statistics.append(mybins)
# Go through the features
for feat in features:
# Allow user abort
if self.abort is True:
break
# Prepare for the histogram creation by extracting
# line geometries (QgsGeometry) from the input layer
# First we do all the lines of the layer. Later we
# will do the lines per tile
# We use a list of line geometries to be able to
# handle MultiPolylines and Polygons
inputlines = []
geom = feat.geometry() # QgsGeometry
if geometryType == QgsWkbTypes.LineGeometry:
# Lines!
if geom.isMultipart():
theparts = geom.constParts()
# QgsGeometryConstPartIterator
# Go through the parts of the multigeometry
for part in theparts:
# QgsAbstractGeometry - QgsLineString
partgeom = QgsGeometry.fromPolyline(part)
inputlines.append(partgeom) # QgsGeometry
else:
inputlines.append(geom)
# There are only two possibilites for geometry type, so
# this elif: could be replaced with an else:
elif geometryType == QgsWkbTypes.PolygonGeometry:
# Polygons!
# We use a list of polygon geometries to be able to
# handle MultiPolygons
inputpolygons = []
if geom.isMultipart():
# Multi polygon
multipoly = geom.asMultiPolygon()
for geompoly in multipoly:
# list of list of QgsPointXY
# abstract geometry -> QgsGeometry polygon
polygeometry = QgsGeometry.fromPolygonXY(geompoly)
inputpolygons.append(polygeometry)
else:
# Non-multi polygon
# Make sure it is a QgsGeometry polygon
singlegeom = geom.asPolygon()
polygeometry = QgsGeometry.fromPolygonXY(singlegeom)
inputpolygons.append(polygeometry) # QgsGeometry
# Add the polygon rings
for polygon in inputpolygons:
# create a list of list of QgsPointXY
poly = polygon.asPolygon()
for ring in poly:
# list of QgsPointXY
# Create a QgsGeometry line
geometryring = QgsGeometry.fromPolylineXY(ring)
inputlines.append(geometryring) # QgsGeometry
else:
# We should never end up here
self.status.emit("Unexpected geometry type!")
# We introduce a list of line geometries for the tiling
tilelinecoll = [None] * (len(tilegeoms) + 1)
# Use the first element to store all the input lines
# (for the over all histogram)
tilelinecoll[0] = inputlines
# Clip the lines based on the tile layer
if self.tilelayer is not None:
i = 1 # The first one is used for the complete dataset
for tile in tilegeoms: # Go through the tiles
# Create a list for the lines in the tile
newlines = []
for linegeom in inputlines:
# QgsGeometry
# Clip
clipres = linegeom.intersection(tile)
if clipres.isEmpty():
continue
if clipres.isMultipart():
# MultiLineString
clipresparts = clipres.constParts()
for clipline in clipresparts:
# Create a QgsGeometry line
linegeom = QgsGeometry.fromPolyline(clipline)
newlines.append(linegeom) # QgsGeometry
else:
# ?
newlines.append(clipres)
tilelinecoll[i] = newlines
i = i + 1
# Do calculations (line length and directions)
j = 0 # Counter for the tiles
for tilelines in tilelinecoll: # Handling the tiles
for inputlinegeom in tilelines: # Handling the lines
# QgsGeometry line - wkbType 2
if inputlinegeom is None:
continue
numvert = 0
for v in inputlinegeom.vertices():
numvert = numvert + 1
if numvert == 0:
continue
if numvert < 2:
self.status.emit("Less than two vertices!")
continue
# Go through all the segments of this line
thispoint = inputlinegeom.vertexAt(0) # QgsPoint
first = True
for v in inputlinegeom.vertices():
if first:
first = False
continue
nextpoint = v
linelength = sqrt(thispoint.distanceSquared(nextpoint))
# Find the angle of the line segment
lineangle = thispoint.azimuth(nextpoint)
if lineangle < 0:
lineangle = 360 + lineangle
if self.directionneutral:
if lineangle >= 180.0:
lineangle = lineangle - 180
# Find the bin
if lineangle > self.offsetangle:
fitbin = (int((lineangle - self.offsetangle) /
self.binsize) % self.bins)
else:
fitbin = (int((360 + lineangle -
self.offsetangle) / self.binsize) %
self.bins)
# Have to handle special case to keep index in range?
if fitbin == self.bins:
self.status.emit("fitbin == self.bins")
fitbin = 0
# Add to the length of the bin of this tile (j)
statistics[j][fitbin][0] = (statistics[j][fitbin][0] +
linelength)
# Add to the number of line segments in the bin
statistics[j][fitbin][1] = (statistics[j][fitbin][1] +
1)
thispoint = nextpoint # advance to the next point
j = j + 1 # Next tile
self.calculate_progress()
except Exception as e:
self.status.emit("Exception occurred - " + str())
self.error.emit(str(e))
self.finished.emit(False, None)
else:
if self.abort:
self.status.emit("Aborted")
self.finished.emit(False, None)
else:
self.status.emit("Completed")
self.finished.emit(True, statistics)
def calculate_progress(self):
'''Update progress and emit a signal with the percentage'''
self.processed = self.processed + 1
# update the progress bar at certain increments
if self.increment == 0 or self.processed % self.increment == 0:
percentage_new = (self.processed * 100) / self.feature_count
if percentage_new > self.percentage:
self.percentage = int(percentage_new)
self.progress.emit(self.percentage)
def kill(self):
'''Kill the thread by setting the abort flag'''
self.abort = True
def tr(self, message):
"""Get the translation for a string using Qt translation API.
We implement this ourselves since we do not inherit QObject.
:param message: String for translation.
:type message: str, QString
:returns: Translated version of message.
:rtype: QString
"""
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
return QCoreApplication.translate('LineDirectionEngine', message)