"""@package OsmMoveMT This module holds all structures and methods required to perform move operation on OSM data. Snapping to existing features is supported when moving a feature. Moving process generates some rubberBands and vertexMarkers so that user can watch the whole action on the map in a nice way. There is also an interaction with plugin's "OSM Feature" dialog. Affected features are dynamically loaded to it; thanks to that user is not confused about what (s)he is moving. """ from PyQt4.QtCore import * from PyQt4.QtGui import * from qgis.core import * from qgis.gui import * from math import * class OsmMoveMT(QgsMapTool): """This class represents map tool for feature moving (see QgsMapTool from Quantum GIS API). It enables to move any OSM feature. User is expected to left click, select and move... The second phase (selecting) is necessary, because there can be more than one feature at the same place. At the beginning of action left-click is used to choose the position on the map. Repeatable right-clicking is then used to select required feature from specified position. After that mouse moving moves selected feature. The last action is left-clicking again (that confirms the moving operation) or right-clicking (canceling operation). Snapping to existing features is supported when moving a feature. When moving a line/polygon, only three closest vertexes to the mouse position can be snapped. If snapping is enabled for all vertexes, operation will be very slow on features with many vertexes. When moving a point (also vertex of line/polygon) snapping to both vertexes and segments is done. When moving a line/polygon snapping to vertexes is supported only. Moving process generates some rubberBands and vertexMarkers so that user can watch the whole action on the map in a nice way. There is also an interaction with plugin's "OSM Feature" dialog. Affected features are dynamically loaded to it; thanks to that user is not confused about what (s)he is moving. Map tool catches the signal of changing OSM database. It such case not-ended operation is canceled. """ def __init__(self, plugin): """The constructor. Initializes the map tool, creates necessary snappers. @param plugin pointer to OSM Plugin instance """ QgsMapTool.__init__(self,plugin.canvas) self.canvas=plugin.canvas self.dockWidget=plugin.dockWidget self.dbm=plugin.dbm self.ur=plugin.undoredo # init info about feature that is being moved! self.mapPointFrom=None self.mapPointTo=None self.movFeatType=None self.movFeat=None self.movIsPolygon=False self.movIndexes=[] self.snapDeltas=None self.snapVertexIx=None self.snapFeat=None self.snapFeatType=None # init variables that keeps system state self.snappingEnabled=True self.doubleclick=False self.movingMode="INTRO" # -> "SELECTION" -> "MOVING" self.moves=0 self.featuresFound=[] self.ixFeature=0 # init objects to display on canvas self.rubBands=[] # rubBands of neighbour (non-point) feats that are also affected by moving operation self.isPolygonFlags=[] # isPolygon flags for self.rubBands[] self.memberIndexes=[] self.snapRubBand=self.__createSnapRubberband() # creating rubberband for snapped objects self.verMarker=self.__createVertexMarker() # creating vertex marker for point moving # creating Vertex & Segment snapper + creating Vertex (only!) snapper self.snapperVS=self.__createVSSnapper(self.canvas.mapRenderer()) self.snapperV=self.__createVSnapper(self.canvas.mapRenderer()) # create dialog with feature selection self.dlgSelect=QDialog(self.dockWidget) self.dlgSelect.setWindowTitle("Feature identification") butBox=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,Qt.Horizontal,self.dlgSelect) self.lw=QListWidget(self.dlgSelect) layout=QVBoxLayout(self.dlgSelect) layout.addWidget(self.lw) layout.addWidget(butBox) self.dlgSelect.setLayout(layout) # set dialog signals QObject.connect(butBox,SIGNAL("accepted()"),self.__onSelectDlgOK) QObject.connect(butBox,SIGNAL("rejected()"),self.__onSelectDlgCancel) self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") def reinit(self): """Function re-initializes the map tool, prepare necessary rubberbands again. After calling this function, move map tool is in the same state as after its creation. """ # reinit info about feature that is being moved! self.mapPointFrom=None self.mapPointTo=None self.movFeatType=None self.movFeat=None self.movIsPolygon=False self.movIndexes=[] self.snapDeltas=None self.snapVertexIx=None self.snapFeat=None self.snapFeatType=None # reinit variables that keeps system state self.snappingEnabled=True self.doubleclick=False self.movingMode="INTRO" # -> "SELECTION" -> "MOVING" self.moves=0 self.featuresFound=[] self.ixFeature=0 # reinit objects to display on canvas for ix in range(0,len(self.rubBands)): self.rubBands[ix].reset(self.isPolygonFlags[ix]) del self.rubBands self.rubBands=[] # rubBands of neighbour feats that are also affected by moving operation del self.isPolygonFlags del self.memberIndexes self.isPolygonFlags=[] # isPolygon flags for self.rubBands[] self.memberIndexes=[] self.snapRubBand.reset() # todo: ??? polygon ??? del self.snapRubBand self.snapRubBand=self.__createSnapRubberband() # recreating rubberband for snapped objects self.verMarker.setCenter(QgsPoint(-1000,-1000)) del self.verMarker self.verMarker=self.__createVertexMarker() # recreating vertex marker for point moving self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") def databaseChanged(self,dbKey): """This function is called automatically when current OSM database has changed. Function calls re-initialization of maptool and create new snappers again. @param dbKey key of database with new current OSM data """ # re-initialization self.reinit() # plus creation of new snappers if dbKey: del self.snapperVS self.snapperVS=self.__createVSSnapper(self.canvas.mapRenderer()) del self.snapperV self.snapperV=self.__createVSnapper(self.canvas.mapRenderer()) def __createFeatRubberband(self,isPolygon): """Function creates rubberband that is used for marking moved feature on the map. @param isPolygon is hint for this function; it says if feature is of polygon type or not @return rubberband for marking moved feature on the map """ # get qgis settings of line width and color for rubberband settings=QSettings() qgsLineWidth=settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() qgsLineRed=settings.value( "/qgis/digitizing/line_color_red", QVariant(255) ).toInt() qgsLineGreen=settings.value( "/qgis/digitizing/line_color_green", QVariant(0) ).toInt() qgsLineBlue=settings.value( "/qgis/digitizing/line_color_blue", QVariant(0) ).toInt() rband=QgsRubberBand(self.canvas,isPolygon) rband.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) rband.setWidth( qgsLineWidth[0] ) return rband def __createSnapRubberband(self): """Function creates rubberband that is used for marking map features to which snapping will be performed. @return rubberband for marking map features """ # get qgis settings of line width and color for rubberband settings=QSettings() qgsLineWidth=settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() rband=QgsRubberBand(self.canvas,False) rband.setColor(QColor(255,0,0)) rband.setWidth(qgsLineWidth[0]) return rband def __createVertexMarker(self): """Function creates vertexMarker that is used for marking moved feature (point) on the map. @return vertex marker for marking moved feature on map """ # get qgis settings settings=QSettings() qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() verMarker=QgsVertexMarker(self.canvas) verMarker.setIconType(2) verMarker.setIconSize(13) verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) verMarker.setPenWidth(qgsLineWidth[0]) verMarker.setCenter(QgsPoint(-1000,-1000)) return verMarker def __createVSSnapper(self,canvasRenderer): """Function creates snapper that snaps within standard qgis tolerance. Snapping of this snapper is done to all segments and vertexes of all three layers of current OSM database. @param canvasRenderer renderer of current map canvas @return instance of vertex+segment QgsSnapper """ if not self.dbm.currentKey: # there is no current database -> no layer for snapping return QgsSnapper(canvasRenderer) snapper=QgsSnapper(canvasRenderer) snapLayers=[] # snap to osm layers from current database only sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) sLayer.mSnapTo=QgsSnapper.SnapToVertex snapLayers.append(sLayer) sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) sLayer.mSnapTo=QgsSnapper.SnapToVertexAndSegment snapLayers.append(sLayer) sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) sLayer.mSnapTo=QgsSnapper.SnapToVertexAndSegment snapLayers.append(sLayer) snapper.setSnapLayers(snapLayers) return snapper def __createVSnapper(self,canvasRenderer): """Function creates snapper that snaps within standard qgis tolerance. Snapping of this snapper is done to all vertexes (but not segments) of all three layers of current OSM database. @param canvasRenderer renderer of current map canvas @return instance of vertex QgsSnapper """ if not self.dbm.currentKey: # there is no current database -> no layer for snapping return QgsSnapper(canvasRenderer) snapper=QgsSnapper(canvasRenderer) snapLayers=[] # snap to osm layers from current database only sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) sLayer.mSnapTo=QgsSnapper.SnapToVertex snapLayers.append(sLayer) sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) sLayer.mSnapTo=QgsSnapper.SnapToVertex snapLayers.append(sLayer) sLayer=QgsSnapper.SnapLayer() sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) sLayer.mSnapTo=QgsSnapper.SnapToVertex snapLayers.append(sLayer) snapper.setSnapLayers(snapLayers) return snapper def deactivate(self): """Functions is called when move-map-tool is being deactivated. Function performs standard cleaning; re-initialization etc. """ self.reinit() self.dockWidget.toolButtons.setExclusive(False) self.dockWidget.moveButton.setChecked(False) self.dockWidget.toolButtons.setExclusive(True) self.dockWidget.activeEditButton=self.dockWidget.dummyButton def keyPressEvent(self, event): """This function is called after keyPressEvent(QKeyEvent *) signal is emmited when using move map tool. If Control key was pressed, function disables snapping til key is released. @param event event that occured when key pressing """ if (event.key() == Qt.Key_Control): self.snappingEnabled=False self.snapRubBand.reset() self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping OFF.") def keyReleaseEvent(self, event): """This function is called after keyReleaseEvent(QKeyEvent *) signal is emmited when using move map tool. If Control key was released, function enables snapping again. @param event event that occured when key releasing """ if (event.key() == Qt.Key_Control): self.snappingEnabled = True self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON - hold Ctrl to disable it.") def canvasDoubleClickEvent(self,event): """This function is called after doubleclick on map when using move map tool. Function is interested into left-doubleclicking only. It finds out all features that are currently at the place where doubleclick was done. Then it shows simple dialog with the list of all these features. User can select the required one, close dialog and continue moving. @param event event that occured when double clicking """ if event.button()<>Qt.LeftButton: return self.dockWidget.clear() #self.removeMarkers() # find out map coordinates from mouse click mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) # display modal dialog with features selection self.featuresFound=self.dbm.findAllFeatures(mapPoint) self.ixFeature=0 lwItems=[] for f in self.featuresFound: feat=f[0] featType=f[1] name=self.dbm.getTagValue(feat.id(),featType,"name") lwItems.append(QString("[%1] ").arg(feat.id()).append(featType).append(QString(" ")).append(name)) self.lw.clear() self.lw.addItems(lwItems) self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") self.doubleclick=True if self.dlgSelect: # continue only if OK button was clicked if self.dlgSelect.exec_()==0: return def canvasReleaseEvent(self, event): """This function is called after mouse button releasing on map when using move map tool. Such button releasing can have a lot of meanings. It depends on the current phase of moving. See documentation of the whole move map tool to know how moving works and how many times user has to release the mouse button to perform the whole moving. @param event event that occured when button releasing """ if self.doubleclick: self.doubleclick=False return if self.movingMode=="INTRO": # we are interested only in left button clicking if event.button()<>Qt.LeftButton: return # find out map coordinates from mouse click self.mapPointFrom = self.dockWidget.canvasToOsmCoords(event.pos()) # what to move? self.featuresFound=self.dbm.findAllFeatures(self.mapPointFrom) self.ixFeature=0 if len(self.featuresFound)>0: self.movingMode="SELECTION" (feat,featType)=self.featuresFound[self.ixFeature] self.dockWidget.loadFeature(feat,featType,2) elif self.dockWidget.feature: self.dockWidget.clear() self.mapPointFrom=None return elif self.movingMode=="SELECTION": # we are interested only in left/right button clicking if event.button() not in (Qt.LeftButton,Qt.RightButton): return if event.button()==Qt.RightButton: if len(self.featuresFound)<1: return self.ixFeature=self.ixFeature+1 if self.ixFeature>=len(self.featuresFound): self.ixFeature=0 (feat,featType)=self.featuresFound[self.ixFeature] self.dockWidget.loadFeature(feat,featType,2) else: # LeftButton self.reinit() if self.dockWidget.feature: self.dockWidget.clear() elif self.movingMode=="MOVING": # finish feature moving # we are interested only in left button clicking; other buttons just cancel moving operation if event.button()<>Qt.LeftButton: self.reinit() return whereIAm = self.dockWidget.canvasToOsmCoords(event.pos()) if self.snapDeltas: self.mapPointTo=QgsPoint(whereIAm.x()+self.snapDeltas[0],whereIAm.y()+self.snapDeltas[1]) else: # no snapping for this moving self.snapVertexIx=-1 self.mapPointTo=whereIAm deltaX=self.mapPointTo.x()-self.mapPointFrom.x() deltaY=self.mapPointTo.y()-self.mapPointFrom.y() self.__finishFeatureMoving(deltaX,deltaY) def __tryIdentifyFeature(self,event): """Function just finds first feature at the place when event occured. Feature is marked on map with rubberBand (or vertexMarker) and is loaded to OSM Feature widget. @param event event that occured """ # find out map coordinates from mouse click mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) feature=self.dbm.findFeature(mapPoint) if feature: (feat,featType)=feature if not self.dockWidget.feature or feat.id()<>self.dockWidget.feature.id(): self.dockWidget.loadFeature(feat,featType,1) elif self.dockWidget.feature: self.dockWidget.clear() def canvasMoveEvent(self,event): """This function is called after mouse moving. Mouse moving is ignored before feature (to move) is selected. @param event event that occured when mouse moving. """ # ignore one move from each two moves if self.moves<>1: self.moves=self.moves+1 return self.moves=0 if self.movingMode=="INTRO": self.__tryIdentifyFeature(event) return if self.movingMode=="SELECTION": # remember what to move self.movFeat=self.featuresFound[self.ixFeature][0] self.movFeatType=self.featuresFound[self.ixFeature][1] self.movIsPolygon=False self.featuresFound=[] self.ixFeature=0 # initializing rubberbands if self.movFeatType in ('Polygon','Line'): layer=self.dbm.lineLayers[self.dbm.currentKey] if self.movFeatType=='Polygon': self.movIsPolygon=True layer=self.dbm.polygonLayers[self.dbm.currentKey] # finding out three closest vertexes (for these snapping will be enabled) self.movIndexes=[] (p,ix,ixB,ixA,dis)=self.movFeat.geometry().closestVertex(self.mapPointFrom) self.movIndexes.append(ix) self.movIndexes.append(ixB) self.movIndexes.append(ixA) rubBand=self.__createFeatRubberband(self.movIsPolygon) rubBand.setToGeometry(self.movFeat.geometry(),layer) self.rubBands.append(rubBand) self.isPolygonFlags.append(self.movIsPolygon) elif self.movFeatType in ('Point'): # find out parent features (parentFeats,self.memberIndexes,self.isPolygonFlags)=self.dbm.getNodeParents(self.movFeat) self.movParentVertices=[] if len(parentFeats)==0: self.verMarker.setCenter(self.movFeat.geometry().asPoint()) for ix in range(0,len(parentFeats)): layer=None if self.isPolygonFlags[ix]: layer=self.dbm.polygonLayers[self.dbm.currentKey] self.movParentVertices=self.movParentVertices+(parentFeats[ix].geometry().asPolygon())[0] else: layer=self.dbm.lineLayers[self.dbm.currentKey] self.movParentVertices=self.movParentVertices+parentFeats[ix].geometry().asPolyline() parentRubBand=self.__createFeatRubberband(self.isPolygonFlags[ix]) parentRubBand.setToGeometry(parentFeats[ix].geometry(),layer) self.rubBands.append(parentRubBand) if self.dockWidget.feature: self.dockWidget.clear() # change moving mode to the last one! self.movingMode="MOVING" # movingMode ~ "MOVING" if self.movFeatType=='Point': (deltaX,deltaY)=self.__getDeltaForPoint(event) # snapping is done in this function targetPoint=QgsPoint(self.mapPointFrom.x()+deltaX,self.mapPointFrom.y()+deltaY) if len(self.rubBands)==0: point=self.movFeat.geometry().asPoint() self.verMarker.setCenter(QgsPoint(point.x()+deltaX,point.y()+deltaY)) # move rubberbands for ix in range(0,len(self.rubBands)): for j in range(0,len(self.memberIndexes[ix])): vertexIx=self.memberIndexes[ix][j] lastVertexIx=self.rubBands[ix].numberOfVertices()-1 self.rubBands[ix].movePoint(vertexIx,targetPoint) if self.isPolygonFlags[ix]: if vertexIx==1: self.rubBands[ix].movePoint(lastVertexIx,targetPoint) elif vertexIx==lastVertexIx: self.rubBands[ix].movePoint(vertexIx,targetPoint) if vertexIx==1: self.rubBands[ix].movePoint(0,targetPoint) elif self.movFeatType in ('Line','Polygon'): (deltaX,deltaY)=self.__getDeltaForLinePolygon(event) # move feature rubberband self.rubBands[0].setTranslationOffset(deltaX,deltaY) def __getDeltaForPoint(self,event): """Function gets an event object, performs snapping from place where event occured and then counts distance of found position from the place where the whole moving operation has started. Special version for points. @param event event that occured """ # find out where and how far (from the place where moving was started) we are now mapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) if not self.snappingEnabled: self.snapDeltas=self.snapFeat=self.snapFeatType=None # returns how far from the place where moving was started we are now return (mapPoint.x()-self.mapPointFrom.x(),mapPoint.y()-self.mapPointFrom.y()) # perform snapping self.movParentVertices.append(self.movFeat.geometry().asPoint()) (retval,snappingResults)=self.snapperVS.snapPoint(event.pos(),self.movParentVertices) if len(snappingResults)==0: self.snapDeltas=self.snapFeat=self.snapFeatType=None # returns how far from the place where moving was started we are now return (mapPoint.x()-self.mapPointFrom.x(),mapPoint.y()-self.mapPointFrom.y()) # we snapped successfully to something snappedPoint=QgsPoint(snappingResults[0].snappedVertex) self.snapDeltas=(snappedPoint.x()-mapPoint.x(),snappedPoint.y()-mapPoint.y()) # use snappingResults[0].layer in findFeature() ??? findFeatureInLayer() ??? (self.snapFeat,self.snapFeatType)=self.dbm.findFeature(snappedPoint) # returns how far from the place where moving was started we are now return (snappedPoint.x()-self.mapPointFrom.x(),snappedPoint.y()-self.mapPointFrom.y()) def __getDeltaForLinePolygon(self,event): """Function gets an event object, performs snapping from place where event occured and then counts distance of found position from the place where the whole moving operation has started. Special version for lines and polygons. @param event event that occured """ # find out where and how far (from the place where moving was started) we are now mapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) deltaX=mapPoint.x()-self.mapPointFrom.x() deltaY=mapPoint.y()-self.mapPointFrom.y() if not self.snappingEnabled: self.snapDeltas=self.snapFeat=self.snapFeatType=None return (deltaX,deltaY) lineMembers=[] for ix in self.movIndexes: lineMembers.append(self.movFeat.geometry().vertexAt(ix)) allMembers=[] if self.movFeatType=='Line': allMembers=self.movFeat.geometry().asPolyline() else: polygon=self.movFeat.geometry().asPolygon() allMembers=polygon[0] minDistance=99999 bestSnappedPoint=None bestActualLineMember=None for i in range(0,len(lineMembers)): actualLineMember=QgsPoint(lineMembers[i].x()+deltaX,lineMembers[i].y()+deltaY) point=self.canvas.getCoordinateTransform().transform(actualLineMember) (retval,snappingResults)=self.snapperV.snapPoint(QPoint(point.x(),point.y()),allMembers) if len(snappingResults)>0: snappedPoint=QgsPoint(snappingResults[0].snappedVertex) dX=snappedPoint.x()-(actualLineMember.x()+deltaX) dY=snappedPoint.y()-(actualLineMember.y()+deltaY) dist=sqrt(pow(dX,2)+pow(dY,2)) # pythagoras ;) if dist