/
vertexTracerTool.py
316 lines (281 loc) · 13.4 KB
/
vertexTracerTool.py
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 -*-
# ---------------------------------------------------------------------
#
# Vertex Trace Tool - A QGIS tool to get a geometry by moving the cursor over vertices. Draws rubberband.
#
# Copyright (C) 2010-2012 Cédric Möri
#
# EMAIL: cmoe (at) geoing (dot) ch
# WEB : www.geoing.ch
#
# ---------------------------------------------------------------------
#
# licensed under the terms of GNU GPL 2
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ---------------------------------------------------------------------
from qgis.PyQt.QtCore import Qt, QPoint, pyqtSignal
from qgis.PyQt.QtGui import QCursor, QPixmap, QColor
from qgis.core import Qgis, QgsMapToPixel, QgsPoint, QgsGeometry, QgsSnappingUtils
from qgis.gui import QgsMapTool, QgsRubberBand
import math
# import pydevd
# Vertex Finder Tool class
class VertexTracerTool(QgsMapTool):
traceFound = pyqtSignal(QgsGeometry)
def __init__(self, canvas):
# some stuff we need from qgis
QgsMapTool.__init__(self, canvas)
self.canvas = canvas
self.snapper = QgsSnappingUtils()
# some stuff to control our state
self.mCtrl = False
self.started = False
self.firstTimeOnSegment = True
self.lastPointMustStay = False
# some stuff to put our temp output
self.lastPoint = None
self.rb = None
self.isPolygon = False
# our own fancy cursor
self.cursor = QCursor(QPixmap(["16 16 3 1",
" c None",
". c #00FF00",
"+ c #FFFFFF",
" ",
" +.+ ",
" ++.++ ",
" +.....+ ",
" +. .+ ",
" +. . .+ ",
" +. . .+ ",
" ++. . .++",
" ... ...+... ...",
" ++. . .++",
" +. . .+ ",
" +. . .+ ",
" ++. .+ ",
" ++.....+ ",
" ++.++ ",
" +.+ "]))
# we need to know, if ctrl-key is pressed
def keyPressEvent(self, event):
if event.key() == Qt.Key_Control:
self.mCtrl = True
# and also if ctrl-key is released
def keyReleaseEvent(self, event):
if event.key() == Qt.Key_Control:
self.mCtrl = False
# remove the last added point when the delete key is pressed
if event.key() == Qt.Key_Backspace:
self.rb.removeLastPoint()
def canvasPressEvent(self, event):
# on left click, we add a point
if event.button() == 1:
layer = self.canvas.currentLayer()
# if it is the start of a new trace, set the rubberband up
if not self.started:
if layer.geometryType() == 1:
self.isPolygon = False
if layer.geometryType() == 2:
self.isPolygon = True
self.rb = QgsRubberBand(self.canvas, layer.geometryType())
self.rb.setColor(QColor(255, 0, 0))
# declare, that are we going to work, now!
self.started = True
if layer is not None:
x = event.pos().x()
y = event.pos().y()
selPoint = QPoint(x, y)
# try to get something snapped
(retval, result) = self.snapper.snapToBackgroundLayers(selPoint)
# the point we want to have, is either from snapping result
if result != []:
point = result[0].snappedVertex
# if we snapped something, it's either a vertex
if result[0].snappedVertexNr != -1:
self.firstTimeOnSegment = True
# or a point on a segment, so we have to declare, that a point on segment is found
else:
self.firstTimeOnSegment = False
# or its some point from out in the wild
else:
point = QgsMapToPixel.toMapCoordinates(self.canvas.getCoordinateTransform(), x, y)
self.firstTimeOnSegment = True
# bring the rubberband to the cursor i.e. the clicked point
self.rb.movePoint(point)
# and set a new point to go on with
self.appendPoint(point)
# try to remember that this point was on purpose i.e. clicked by the user
self.lastPointMustStay = True
self.firstTimeOnSegment = True
def canvasMoveEvent(self, event):
# QgsMessageLog.logMessage('fts: '+str(self.firstTimeOnSegment), 'state', 0)
# QgsMessageLog.logMessage('lpc: '+str(self.lastPointMustStay), 'state', 0)
if self.started:
# Get the click
x = event.pos().x()
y = event.pos().y()
eventPoint = QPoint(x, y)
# only if the ctrl key is pressed
if self.mCtrl:
layer = self.canvas.currentLayer()
if layer is not None:
(retval, result) = self.snapper.snapToBackgroundLayers(eventPoint)
# so if we have found a snapping
if result != []:
# pydevd.settrace()
# that's the snapped point
point = QgsPoint(result[0].snappedVertex)
# if it is a vertex, not a point on a segment
if result[0].snappedVertexNr != -1:
self.rb.movePoint(point)
self.appendPoint(point)
self.lastPointMustStay = True
# the next point found, may be on a segment
self.firstTimeOnSegment = True
# we are on a segment
else:
self.rb.movePoint(point)
# if we are on a new segment, we add the point in any case
if self.firstTimeOnSegment:
self.appendPoint(point)
self.lastPointMustStay = True
self.firstTimeOnSegment = False
# if we are not on a new segemnt, we have to test, if this point is realy needed
else:
# but only if we have already enough points
# TODO: check if this is correct for lines also (they only need two points, to be vaild)
if self.rb.numberOfVertices() >= 3:
max = self.rb.numberOfVertices()
lastRbP = self.rb.getPoint(0, max - 2)
# QgsMessageLog.logMessage(str(self.rb.getPoint(0, max-1)), 'rb', 0)
nextToLastRbP = self.rb.getPoint(0, max - 3)
# QgsMessageLog.logMessage(str(self.rb.getPoint(0, max-2)), 'rb', 0)
# QgsMessageLog.logMessage(str(point), 'rb', 0)
if not self.pointOnLine(lastRbP, nextToLastRbP, QgsPoint(point)):
self.appendPoint(point)
self.lastPointMustStay = False
else:
# TODO: schauen, ob der letzte punkt ein klick war, dann nicht löschen. entsrpechend auch die "punkt auf linie" neu berechenen.
if not self.lastPointMustStay:
self.rb.removeLastPoint()
self.rb.movePoint(point)
# QgsMessageLog.logMessage('point removed', 'click', 0)
# else:
# QgsMessageLog.logMessage('sleep', 'rb', 0)
else:
self.appendPoint(point)
self.lastPointMustStay = False
self.firstTimeOnSegment = False
else:
# if nothing specials happens, just update the rubberband to the cursor position
point = QgsMapToPixel.toMapCoordinates(self.canvas.getCoordinateTransform(), x, y)
self.rb.movePoint(point)
else:
# in "not-tracing" state, just update the rubberband to the cursor position
# but we have still to snap to act like the "normal" digitize tool
(retval, result) = self.snapper.snapToBackgroundLayers(eventPoint)
if result != []:
point = QgsPoint(result[0].snappedVertex)
else:
point = QgsMapToPixel.toMapCoordinates(self.canvas.getCoordinateTransform(), x, y)
self.rb.movePoint(point)
def canvasReleaseEvent(self, event):
# with right click the digitizing is finished
if event.button() == 2:
layer = self.canvas.currentLayer()
x = event.pos().x()
y = event.pos().y()
if layer is not None and self.started:
selPoint = QPoint(x, y)
(retval, result) = self.snapper.snapToBackgroundLayers(selPoint)
if result != []:
point = result[0].snappedVertex
else:
point = QgsMapToPixel.toMapCoordinates(self.canvas.getCoordinateTransform(), x, y)
# add this last point
self.appendPoint(QgsPoint(point))
self.sendGeometry()
def appendPoint(self, point):
# don't add the point if it is identical to the last point we added
if not (self.lastPoint == point):
self.rb.addPoint(point)
self.lastPoint = QgsPoint(point)
else:
pass
# see: double QgsGeometryValidator::distLine2Point( QgsPoint p, QgsVector v, QgsPoint q )
# distance of point q from line through p in direction v
def pointOnLine(self, pntAft, pntBef, pntTest):
p = QgsPoint(pntAft)
vx = pntBef.x() - pntAft.x()
vy = pntBef.y() - pntAft.y()
vlength = math.sqrt(vy * vy + vx * vx)
if vlength == 0:
return False
q = QgsPoint(pntTest)
res = (vx * (q.y() - p.y()) - vy * (q.x() - p.x())) / vlength
# res = 0 means point is on line, but we are not in a perfect world, so a tolerance is needed
# to get rid of some numerical problems. Tolerance estimated by solid engenieering work (rule of thumb...)
if res < 1E-10:
return True
else:
return False
def sendGeometry(self):
layer = self.canvas.currentLayer()
coords = []
# backward compatiblity for a bug in qgsRubberband, that was fixed in 1.7
if Qgis.QGIS_VERSION_INT >= 10700:
[coords.append(self.rb.getPoint(0, i)) for i in range(self.rb.numberOfVertices())]
else:
[coords.append(self.rb.getPoint(0, i)) for i in range(1, self.rb.numberOfVertices())]
# On the Fly reprojection, not necessary any more, mapToLayerCoordinates is clever enough on its own
# layerEPSG = layer.srs().epsg()
# projectEPSG = self.canvas.mapRenderer().destinationSrs().epsg()
# if layerEPSG != projectEPSG:
coords_tmp = coords[:]
coords = []
for point in coords_tmp:
transformedPoint = self.canvas.mapRenderer().mapToLayerCoordinates(layer, point)
coords.append(transformedPoint)
coords_tmp = coords[:]
coords = []
lastPt = None
for pt in coords_tmp:
if (lastPt != pt):
coords.append(pt)
lastPt = pt
# Add geometry to feature.
if self.isPolygon:
g = QgsGeometry().fromPolygon([coords])
else:
g = QgsGeometry().fromPolyline(coords)
self.traceFound.emit(g)
self.rb.reset(self.isPolygon)
self.started = False
def activate(self):
self.canvas.setCursor(self.cursor)
def deactivate(self):
try:
self.rb.reset()
except AttributeError:
pass
def isZoomTool(self):
return False
def isTransient(self):
return False
def isEditTool(self):
return True