def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandSegment1 = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSegment1.setColor(QColor(255, 0, 0)) self.rubberBandSegment1.setWidth(2) self.layer = None self.reset()
def __init__(self, iface, toolBar): # Save reference to the QGIS interface self.iface = iface self.canvas = self.iface.mapCanvas() # the 4 points of the 2 segments self.p11 = None self.p12 = None self.p21 = None self.p22 = None # Create actions self.act_intersection = QAction( QIcon(":/plugins/cadtools/icons/pointandline.png"), QCoreApplication.translate("ctools", "Intersection Point"), self.iface.mainWindow()) self.act_s2s = QAction( QIcon(":/plugins/cadtools/icons/select2lines.png"), QCoreApplication.translate("ctools", "Select 2 Line Segments"), self.iface.mainWindow()) self.act_s2s.setCheckable(True) # Connect to signals for button behaviour self.act_s2s.triggered.connect(self.s2s) self.act_intersection.triggered.connect(self.intersection) self.canvas.mapToolSet.connect(self.deactivate) # Add actions to the toolbar toolBar.addAction(self.act_s2s) toolBar.addAction(self.act_intersection) #toolBar.addSeparator() # Get the tool self.tool = SegmentFinderTool(self.canvas)
def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) # just needs to rubberbands (when selecting 2nd segment, replaced by selectedLine or # hidden) self.rubberBandSelectedSegment = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSelectedSegment.setColor(QColor(255, 0, 0)) self.rubberBandSelectedSegment.setWidth(2) self.rubberBandSelectedLine = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSelectedLine.setColor(QColor(255, 0, 0)) self.rubberBandSelectedLine.setWidth(2) self.defaultIndex = None self.layer = None self.reset()
class CenterlineDigitizingMode(QgsMapToolEmitPoint): def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandSelectedSegment = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSelectedSegment.setColor(QColor(255, 0, 0)) self.rubberBandSelectedSegment.setWidth(2) self.defaultIndex = None self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages=True): self.step = 0 self.snappingResultFirstEdge = None self.snappingResultSecondEdge = None self.defaultIndex = None self.rubberBandSelectedSegment.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def resetCenterline(self): self.step = 0 self.snappingResultFirstEdge = None self.snappingResultSecondEdge = None self.rubberBandSelectedSegment.reset(QGis.Line) self.segmentFinderTool.layers = None def deactivate(self): self.reset() QgsMapToolEmitPoint.deactivate(self) def next(self): if self.step == 0: self.messageBarUtils.showButton("Centerline", "Select first edge", "Done", buttonCallback=self.done) _, candidateLayers, _ = self.listCandidateLayers( onlyEditable=False) self.segmentFinderTool.layers = candidateLayers self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.firstEdgeFound) elif self.step == 1: candidates, candidateLayers, defaultIndex = self.listCandidateLayers( ) if self.defaultIndex <> None and defaultIndex < len( candidateLayers): defaultIndex = self.defaultIndex _, combobox = self.messageBarUtils.showCombobox( "Centerline", "Select second edge", candidates, defaultIndex) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect( partial(self.secondEdgeFound, candidateLayers, combobox)) elif self.step == 2: self.doCenterline() def firstEdgeFound(self, result, pointClicked): self.snappingResultFirstEdge = result try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass self.segmentFinderTool.deactivate() self.rubberBandSelectedSegment.reset(QGis.Line) self.rubberBandSelectedSegment.addPoint(result.beforeVertex) self.rubberBandSelectedSegment.addPoint(result.afterVertex, True) self.rubberBandSelectedSegment.show() self.step = 1 self.next() def secondEdgeFound(self, candidateLayers, combobox, result, pointClicked): index = combobox.currentIndex() self.defaultIndex = index self.outputLayer = candidateLayers[index] self.snappingResultSecondEdge = result try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass self.segmentFinderTool.deactivate() self.step = 2 self.next() def listCandidateLayers(self, onlyEditable=True): candidates = [] candidateLayers = [] defaultIndex = 0 for layer in QgsMapLayerRegistry.instance().mapLayers().values(): if (layer.type() == QgsMapLayer.VectorLayer and layer.geometryType() == QGis.Line and (not onlyEditable or layer.isEditable())): candidates.append(layer.name()) candidateLayers.append(layer) sortedCandidates = sorted(enumerate(candidates), key=itemgetter(1)) candidates = map(itemgetter(1), sortedCandidates) candidateLayers = map(lambda x: candidateLayers[x[0]], sortedCandidates) for index in range(len(candidateLayers)): if candidateLayers[index] == self.layer: defaultIndex = index return candidates, candidateLayers, defaultIndex def doCenterline(self): self.messageBarUtils.showMessage("Centerline", "Running...", duration=0) try: qDebug("New Centerline") firstSegment = self._getSegment(self.snappingResultFirstEdge) secondSegment = self._getSegment(self.snappingResultSecondEdge) endSegments = self._getEndSegments(firstSegment, secondSegment) # middle of each end segments centerlineEndPoint1 = QgsPoint( (endSegments[0].vertexAt(0).x() + endSegments[0].vertexAt(1).x()) / 2, (endSegments[0].vertexAt(0).y() + endSegments[0].vertexAt(1).y()) / 2) centerlineEndPoint2 = QgsPoint( (endSegments[1].vertexAt(0).x() + endSegments[1].vertexAt(1).x()) / 2, (endSegments[1].vertexAt(0).y() + endSegments[1].vertexAt(1).y()) / 2) line = QgsGeometry.fromPolyline( [centerlineEndPoint1, centerlineEndPoint2]) self.outputLayer.beginEditCommand("Centerline") feature = QgsFeature() feature.initAttributes( self.outputLayer.dataProvider().fields().count()) feature.setGeometry(line) self.outputLayer.addFeature(feature) self.outputLayer.endEditCommand() self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage( "Centerline", "The centerline was created successfully", duration=2) except Exception as e: QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.showMessage( "Centerline", "There was an error performing this command. See QGIS Message log for details", QgsMessageBar.CRITICAL, duration=5) self.outputLayer.destroyEditCommand() self.done() self.resetCenterline() self.next() def _getEndSegments(self, firstSegment, secondSegment): candidates1 = (QgsGeometry.fromPolyline( [firstSegment.vertexAt(0), secondSegment.vertexAt(0)]), QgsGeometry.fromPolyline([ firstSegment.vertexAt(1), secondSegment.vertexAt(1) ])) candidates2 = (QgsGeometry.fromPolyline( [firstSegment.vertexAt(0), secondSegment.vertexAt(1)]), QgsGeometry.fromPolyline([ firstSegment.vertexAt(1), secondSegment.vertexAt(0) ])) if candidates1[0].intersects(candidates1[1]): return candidates2 else: return candidates1 def _getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = snappingResult.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def _getSegment(self, snappingResult): feature = self._getFeature(snappingResult) geometry = feature.geometry() bv = geometry.vertexAt(snappingResult.beforeVertexNr) av = geometry.vertexAt(snappingResult.afterVertexNr) return QgsGeometry.fromPolyline([bv, av]) def done(self): self.reset() self.iface.mapCanvas().unsetMapTool(self) def enter(self): #ignore pass def canvasPressEvent(self, e): # use right button instead of clicking on button in message bar if e.button() == Qt.RightButton: if self.step == 0: self.done() return if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e)
class CenterlineAdvancedDigitizingMode(QgsMapToolEmitPoint): def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) # just needs to rubberbands (when selecting 2nd segment, replaced by selectedLine or # hidden) self.rubberBandSelectedSegment = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSelectedSegment.setColor(QColor(255, 0, 0)) self.rubberBandSelectedSegment.setWidth(2) self.rubberBandSelectedLine = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSelectedLine.setColor(QColor(255, 0, 0)) self.rubberBandSelectedLine.setWidth(2) self.defaultIndex = None self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages=True): self.step = 0 self.snappingResults = [] self.snappingPointsClicked = [] self.subPolylines = [] self.defaultIndex = None self.orderSelection = False self.rubberBandSelectedSegment.reset(QGis.Line) self.rubberBandSelectedLine.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def resetCenterline(self): self.step = 0 self.snappingResults = [] self.snappingPointsClicked = [] self.subPolylines = [] self.rubberBandSelectedSegment.reset(QGis.Line) self.rubberBandSelectedLine.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass def deactivate(self): self.reset() QgsMapToolEmitPoint.deactivate(self) def next(self): if self.step == 0: self.messageBarUtils.showButton( "Centerline", "Select starting segment of first line", "Done", buttonCallback=self.done) _, candidateLayers, _ = self.listCandidateLayers( onlyEditable=False) self.segmentFinderTool.layers = candidateLayers self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.edgeFound) elif self.step == 1: self.messageBarUtils.showMessage( "Centerline", "Select end segment of first line", duration=0) _, candidateLayers, _ = self.listCandidateLayers( onlyEditable=False) self.segmentFinderTool.layers = candidateLayers self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.edgeFound) elif self.step == 2: self.messageBarUtils.showMessage( "Centerline", "Select starting segment of second line", duration=0) _, candidateLayers, _ = self.listCandidateLayers( onlyEditable=False) self.segmentFinderTool.layers = candidateLayers self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.edgeFound) elif self.step == 3: candidates, candidateLayers, defaultIndex = self.listCandidateLayers( onlyEditable=False) if self.defaultIndex <> None and defaultIndex < len( candidateLayers): defaultIndex = self.defaultIndex _, combobox = self.messageBarUtils.showCombobox( "Centerline", "Select end segment of second line", candidates, defaultIndex) self.segmentFinderTool.layers = candidateLayers self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect( partial(self.lastEdgeFound, candidateLayers, combobox)) elif self.step == 4: self.doCenterline() def edgeFound(self, result, pointClicked): try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass self.segmentFinderTool.deactivate() if self.step == 0 or self.step == 2: self.snappingResults.append(result) self.snappingPointsClicked.append(pointClicked) self.rubberBandSelectedSegment.reset(QGis.Line) self.rubberBandSelectedSegment.addPoint(result.beforeVertex) self.rubberBandSelectedSegment.addPoint(result.afterVertex, True) self.rubberBandSelectedSegment.show() self.step += 1 else: # step 1 or 3 p = self._getSubPolyline(self.snappingResults[-1], result, self.snappingPointsClicked[0]) if p: self.subPolylines.append(p) self.snappingResults.append(result) self.snappingPointsClicked.append(pointClicked) self.rubberBandSelectedSegment.reset(QGis.Line) self.rubberBandSelectedLine.reset(QGis.Line) if self.step == 1: # don't draw when step is 3 polyline = QgsGeometry.fromPolyline(p) self.rubberBandSelectedLine.setToGeometry( polyline, self.snappingResults[0].layer) self.rubberBandSelectedLine.show() self.step += 1 else: # not valid choice for second segment: not on same layer or feature or subpolyline (if multipolyline) self.messageBarUtils.showMessage( "Centerline", "Invalid choice for second segment: Select a segment in the same part of the same geometry as the first", QgsMessageBar.WARNING, duration=5) self.next() def lastEdgeFound(self, candidateLayers, combobox, result, pointClicked): try: index = combobox.currentIndex() self.defaultIndex = index self.outputLayer = candidateLayers[index] except: # can happen in message with comobobox is closed by the user self.outputLayer = candidateLayers[self.defaultIndex] self.edgeFound(result, pointClicked) def listCandidateLayers(self, onlyEditable=True): candidates = [] candidateLayers = [] defaultIndex = 0 for layer in QgsMapLayerRegistry.instance().mapLayers().values(): if (layer.type() == QgsMapLayer.VectorLayer and layer.geometryType() == QGis.Line and (not onlyEditable or layer.isEditable())): candidates.append(layer.name()) candidateLayers.append(layer) sortedCandidates = sorted(enumerate(candidates), key=itemgetter(1)) candidates = map(itemgetter(1), sortedCandidates) candidateLayers = map(lambda x: candidateLayers[x[0]], sortedCandidates) for index in range(len(candidateLayers)): if candidateLayers[index] == self.layer: defaultIndex = index return candidates, candidateLayers, defaultIndex def doCenterline(self): self.messageBarUtils.showMessage("Centerline", "Running...", duration=0) try: qDebug("New Centerline Advanced") p1 = self.subPolylines[0] p2 = self.subPolylines[1] if self._mustReverse(p1, p2): # reverse the node order of any of the 2 p1 = list(reversed(p1)) # both input geometries and output layer are in same CRS if len(p1) == len(p2): p = self._processSimple(p1, p2) else: # voronoi with post processing p = self._processVoronoiPolygons(p1, p2) if p: self.outputLayer.beginEditCommand("Centerline") fieldCount = self.outputLayer.dataProvider().fields().count() feature = QgsFeature() feature.initAttributes(fieldCount) feature.setGeometry(p) self.outputLayer.addFeature(feature) self.outputLayer.endEditCommand() self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage( "Centerline", "The centerline was created successfully", duration=2) else: self.messageBarUtils.showMessage("Centerline", "No centerline was created", QgsMessageBar.WARNING, duration=2) except Exception as e: raise QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.showMessage( "Centerline", "There was an error performing this command. See QGIS Message log for details", QgsMessageBar.CRITICAL, duration=5) self.outputLayer.destroyEditCommand() self.done() self.resetCenterline() self.next() def _processSimple(self, p1, p2): #just iterate p = [] for i in range(len(p1)): endSegment = [p1[i], p2[i]] # middle of each end segments centerlineEndPoint = QgsPoint( (endSegment[0].x() + endSegment[1].x()) / 2, (endSegment[0].y() + endSegment[1].y()) / 2) p.append(centerlineEndPoint) return QgsGeometry.fromPolyline(p) def _processVoronoiPolygons(self, p1, p2): tempDir = None try: # define a polygon formed by p1 and p2: add edge at both ends to # close it. P1 and p2 have already been ordered to face each other # in the same direction ring = p1[0:] ring.extend([p1[-1], p2[-1]]) ring.extend(p2[-1::-1]) ring.extend([p2[0], p1[0]]) pipePolygon = QgsGeometry.fromPolygon([ring]) end1 = QgsGeometry.fromPolyline([p1[-1], p2[-1]]) end2 = QgsGeometry.fromPolyline([p2[0], p1[0]]) # create temp shp tempDir = tempfile.mkdtemp() vl = QgsVectorLayer( "LineString?crs=%s" % self.outputLayer.crs().toWkt(), "temp", "memory") pr = vl.dataProvider() vl.startEditing() pr.addAttributes([QgsField("id", QVariant.Int, "Integer")]) vl.commitChanges() vl.startEditing() for p in [p1, p2]: feature = QgsFeature() feature.initAttributes(1) feature.setGeometry(QgsGeometry.fromPolyline(p)) vl.addFeature(feature) vl.commitChanges() qDebug("mem " + repr(vl.featureCount())) inputShp = os.path.join(tempDir, "input.shp") QgsVectorFileWriter.writeAsVectorFormat(vl, inputShp, "UTF-8", vl.crs()) # vl=QgsVectorLayer("Polygon?crs=%s" % self.outputLayer.crs().toWkt(), "temp3", "memory") # pr = vl.dataProvider() # vl.startEditing() # pr.addAttributes([QgsField("id",QVariant.Int,"Integer")]) # vl.commitChanges() # vl.startEditing() # feature=QgsFeature() # feature.initAttributes(1) # feature.setGeometry(pipePolygon) # vl.addFeature(feature) # vl.commitChanges() # QgsMapLayerRegistry.instance().addMapLayer(vl) # processing often fails randomly # try multiple times trials = 0 success = False while trials < 3 and not success: try: output = processing.runalg("qgis:densifygeometries", inputShp, 100, None) output = processing.runalg("qgis:extractnodes", output["OUTPUT"], None) output = processing.runalg("qgis:voronoipolygons", output["OUTPUT"], 0.001, None) voronoi = QgsVectorLayer(output["OUTPUT"], "voronoi", "ogr") # QgsMapLayerRegistry.instance().addMapLayer(voronoi, addToLegend=True) # voronoi.setLayerTransparency(50) output = processing.runalg("qgis:extractnodes", inputShp, None) originalNodes = QgsVectorLayer(output["OUTPUT"], "original", "ogr") success = originalNodes.featureCount > 0 and voronoi.featureCount( ) > 0 except: trials += 1 if not success: return None qDebug("after processing") # 1. create a spatial index with all the voronoi polygons # 2. Get the lsit of edges from the vornoi polygons that are in the pipePolygon voronoiEdges = [] voronoiEdgeInsideSet = set() spatialIndex = QgsSpatialIndex() for f in voronoi.getFeatures(): spatialIndex.insertFeature(f) geometry = f.geometry() # voronoi polygons are single parts with one ring p = geometry.asPolygon() for edgeP in self._extractEdgesFromPolyline(p[0]): edge = QgsGeometry.fromPolyline(edgeP) if edge.within(pipePolygon): key = self._keyFromEdge(edgeP) if key not in voronoiEdgeInsideSet: # prevent duplicates (voronoi polygons share a border) voronoiEdges.append(edgeP) voronoiEdgeInsideSet.add(key) voronoiPolygonMajorEdges = [] qDebug(repr(originalNodes.featureCount())) for originalNode in originalNodes.getFeatures(): geometry = originalNode.geometry() candidates = spatialIndex.intersects(geometry.boundingBox()) if len(candidates) > 0: for candidate in candidates: fCandidate = voronoi.getFeatures( QgsFeatureRequest(candidate)).next() gCandidate = fCandidate.geometry() if geometry.within(gCandidate): p = gCandidate.asPolygon() edges = self._extractEdgesFromPolyline(p[0]) for edge in edges: key = self._keyFromEdge(edge) if key not in voronoiEdgeInsideSet: # do not add the edge if already in the centerline #gEdge=QgsGeometry.fromPolyline(edge) #if gEdge.intersects(pipePolygon): voronoiPolygonMajorEdges.append(key) break qDebug("after spatindex") # recursively combine voronoiedges into one big polyline voronoiEdges = QgsGeometry.fromMultiPolyline(voronoiEdges) centerlinePolyline = voronoiEdges.combine( voronoiEdges) #self.union(voronoiEdges) if centerlinePolyline.isMultipart(): lines = centerlinePolyline.asMultiPolyline() else: lines = [centerlinePolyline.asPolyline()] qDebug("after merge") # key template kT = "%3.11f" nodeToVertexIndex = {} nodeCount = {} for i in range(len(lines)): line = lines[i] for j in range(len(line)): point = line[j] key = (kT % point.x(), kT % point.y()) count = nodeCount.get(key, 0) if j <> 0 and j <> len(line) - 1: # each of those nodes is linked to 2 segments count += 2 else: count += 1 nodeCount[key] = count nodeToVertexIndex[key] = (i, j) # for i in range(len(lines)): # line=lines[i] # for j in range(len(line)): # point=line[j] # key=(kT%point.x(),kT%point.y()) # qDebug(repr(key)+" "+repr(nodeCount[key])) for edgeKey in voronoiPolygonMajorEdges: p1x, p1y, p2x, p2y = edgeKey for key in [(kT % p1x, kT % p1y), (kT % p2x, kT % p2y)]: if key == ('-76.87093621506', '42.15309787572'): qDebug("found mine " + repr(nodeCount.get(key, 0))) count = nodeCount.get(key, 0) if count == 2: # only counts if in the main segment (not the branches) nodeCount[key] = count + 1 qDebug("after indexing") # cleanup: only keep important vertices toKeep = set(filter(lambda x: nodeCount[x] > 2, nodeCount.keys())) toRemove = set(nodeToVertexIndex.keys()) - toKeep toRemoveIndices = map(lambda x: nodeToVertexIndex[x], toRemove) for lineIndex, pointIndex in sorted(toRemoveIndices, cmp=self._compareIndices, reverse=True): line = lines[lineIndex] del line[pointIndex] self._cleanupDegenerateSegments(lines) qDebug("after cleanup") return self.union(map(lambda x: QgsGeometry.fromPolyline(x), lines)) finally: if tempDir: pass #shutil.rmtree(tempDir, True) qDebug("after union") def union(self, lines): return reduce(lambda m, x: m.combine(x), lines[1:], lines[0]) def _compareIndices(self, x, y): i1, j1 = x i2, j2 = y if i1 < i2: return -1 elif i2 < i1: return 1 else: if j1 < j2: return -1 elif j2 < j1: return 1 else: return 0 def _keyFromEdge(self, edge): return (edge[0].x(), edge[0].y(), edge[1].x(), edge[1].y()) def _cleanupDegenerateSegments(self, lines): linesToRemove = [] for i in range(len(lines)): line = lines[i] if len(line) <= 1: linesToRemove.append(i) for i in reversed(linesToRemove): del lines[i] return lines def _extractEdgesFromPolyline(self, p): edges = [] for i in range(len(p) - 1): edgeP = p[i:i + 2] edges.append(edgeP) return edges def _mustReverse(self, p1, p2): polyline1 = QgsGeometry.fromPolyline(p1) polyline2 = QgsGeometry.fromPolyline(p2) candidates1 = (QgsGeometry.fromPolyline([p1[0], p2[0]]), QgsGeometry.fromPolyline([p1[-1], p2[-1]])) return ((candidates1[0].intersects(candidates1[1]) and not candidates1[0].equals(candidates1[1])) or polyline1.crosses(candidates1[0]) or polyline1.crosses(candidates1[1]) or polyline2.crosses(candidates1[0]) or polyline2.crosses(candidates1[1])) def _getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = snappingResult.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def _getSegment(self, snappingResult): feature = self._getFeature(snappingResult) geometry = feature.geometry() bv = geometry.vertexAt(snappingResult.beforeVertexNr) av = geometry.vertexAt(snappingResult.afterVertexNr) return QgsGeometry.fromPolyline([bv, av]) def _getSubPolyline(self, snappingResult1, snappingResult2, pointClicked1): """ computes the polyline defined by the 2 segments: all the segments between those 2 will be included in the returned polyline. If Ctrl held while selecting the 2nd segment, The side of the first segment the user clicked on determines which direction to go to add those segments """ if (snappingResult1.layer.id() <> snappingResult2.layer.id() or snappingResult1.snappedAtGeometry <> snappingResult2.snappedAtGeometry): return None feature = self._getFeature(snappingResult1) geometry = feature.geometry() if geometry.isMultipart(): mp = geometry.asMultipolyline() p, relative = vectorlayerutils.polylineWithVertexAtIndex( mp, snappingResult1.beforeVertexNr) _, relative2 = vectorlayerutils.polylineWithVertexAtIndex( mp, snappingResult2.beforeVertexNr) if relative <> relative2: # not on the same sub polyline return None else: p = geometry.asPolyline() relative = 0 if self.orderSelection: bv = geometry.vertexAt(snappingResult1.beforeVertexNr) av = geometry.vertexAt(snappingResult1.afterVertexNr) # get the direction in which to add segments # vertex closest to point clicked is the top vertex: direction will be from # the other vertex to that one extendUtils = ExtendUtils(self.iface) vertexNrDirection = extendUtils.vertexIndexToMove(bv, av, snappingResult1, pointClicked1, mustExtend=False) # positive if index of vertices is incremented direction = vertexNrDirection == snappingResult1.afterVertexNr if not direction: # swap temp = snappingResult2 snappingResult2 = snappingResult1 snappingResult1 = temp if snappingResult2.afterVertexNr <= snappingResult1.beforeVertexNr: # wrap around nodes = p[snappingResult1.beforeVertexNr - relative:] nodes.extend(p[0:snappingResult2.afterVertexNr - relative + 1]) else: nodes = p[snappingResult1.beforeVertexNr - relative:snappingResult2.afterVertexNr - relative + 1] else: if snappingResult2.beforeVertexNr < snappingResult1.beforeVertexNr: # swap temp = snappingResult2 snappingResult2 = snappingResult1 snappingResult1 = temp direction = False else: direction = True nodes = p[snappingResult1.beforeVertexNr - relative:snappingResult2.afterVertexNr - relative + 1] qDebug(repr(nodes)) return nodes if direction else list(reversed(nodes)) def done(self): self.reset() self.iface.mapCanvas().unsetMapTool(self) def enter(self): #ignore pass def canvasPressEvent(self, e): # use right button instead of clicking on button in message bar if e.button() == Qt.RightButton: if self.step == 0: self.done() return self.orderSelection = e.modifiers() == Qt.ControlModifier qDebug(repr(self.orderSelection)) if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e) def snap(self, pos): (_, result) = self.snapper.snapToBackgroundLayers(pos) return result
class FilletDigitizingMode(QgsMapToolEmitPoint): MESSAGE_HEADER = "Fillet" DEFAULT_FILLET_RADIUS = 0 def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandSegment1 = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandSegment1.setColor(QColor(255, 0, 0)) self.rubberBandSegment1.setWidth(2) self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages=True): self.step = 0 self.segment1 = None self.segment2 = None self.rubberBandSegment1.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def resetFillet(self): self.reset(False) def deactivate(self): self.reset() QgsMapToolEmitPoint.deactivate(self) def next(self): if self.step == 0: self.messageBarUtils.showButton( FilletDigitizingMode.MESSAGE_HEADER, "Select first segment", "Done", buttonCallback=self.done) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.segment1Found) elif self.step == 1: filletRadius, found = QgsProject.instance().readEntry( constants.SETTINGS_KEY, constants.SETTINGS_FILLET_RADIUS, None) if not found: filletRadius = str(FilletDigitizingMode.DEFAULT_FILLET_RADIUS) _, lineEdit = self.messageBarUtils.showLineEdit( FilletDigitizingMode.MESSAGE_HEADER, "Select second segment and set radius:", filletRadius) self.lineEditFilletRadius = lineEdit self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.segment2Found) elif self.step == 2: self.filletRadius = self.lineEditFilletRadius.text() self.doFillet() def segment1Found(self, result, pointClicked): self.segment1 = result self.segmentFinderTool.segmentFound.disconnect(self.segment1Found) self.segmentFinderTool.deactivate() self.rubberBandSegment1.reset(QGis.Line) self.rubberBandSegment1.addPoint(result.beforeVertex) self.rubberBandSegment1.addPoint(result.afterVertex, True) self.rubberBandSegment1.show() self.step = 1 self.next() def segment2Found(self, result, pointClicked): self.segment2 = result self.segmentFinderTool.segmentFound.disconnect(self.segment2Found) self.segmentFinderTool.deactivate() self.step = 2 self.next() def doFillet(self): self.messageBarUtils.showMessage(FilletDigitizingMode.MESSAGE_HEADER, "Running...", QgsMessageBar.INFO, duration=0) try: crsDest = self.layer.crs() canvas = self.iface.mapCanvas() mapRenderer = canvas.mapSettings() crsSrc = mapRenderer.destinationCrs() crsTransform = QgsCoordinateTransform(crsSrc, crsDest) self.layer.beginEditCommand(FilletDigitizingMode.MESSAGE_HEADER) try: filletRadius = float(self.filletRadius) # save for next time QgsProject.instance().writeEntry( constants.SETTINGS_KEY, constants.SETTINGS_FILLET_RADIUS, filletRadius) except ValueError: filletRadius = FilletDigitizingMode.DEFAULT_FILLET_RADIUS # do initial computations in map canvas coordinates # check intersection extendUtils = ExtendUtils(self.iface) ip = extendUtils.intersectionPoint(self.segment1.beforeVertex, self.segment1.afterVertex, self.segment2.beforeVertex, self.segment2.afterVertex) if ip == None: self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "The 2 segments do not intersect. Nothing was done.", QgsMessageBar.WARNING, duration=5) self.layer.destroyEditCommand() return if filletRadius == 0: # reproject to layer coordinates ip = crsTransform.transform(ip) # just extend the 2 lines ok1 = self.extend(ip, self.segment1, extendUtils) ok2 = self.extend(ip, self.segment2, extendUtils) if ok1 and ok2: self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "Success", QgsMessageBar.INFO, 5) self.layer.endEditCommand() else: self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "Cannot fillet segments: Amibiguous extension", QgsMessageBar.WARNING, 5) self.layer.destroyEditCommand() else: (pa, p1, p2, revert1, revert2) = self.computeArcParameters( ip, self.segment1, self.segment2, filletRadius, extendUtils) if pa == None: # radius is too big self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "A fillet cannot be created with those arcs.", QgsMessageBar.WARNING, duration=5) return settings = QSettings() method = settings.value(constants.ARC_METHOD, "") if method == constants.ARC_METHOD_NUMBEROFPOINTS: value = settings.value(constants.ARC_NUMBEROFPOINTS, 0, type=int) else: value = settings.value(constants.ARC_ANGLE, 0, type=float) # g is still in CRS of map g = CircularArc.getInterpolatedArc(p1, pa, p2, method, value) arcVertices = g.asPolyline() if len(arcVertices) >= 2: projArcVertices = [] for vertex in arcVertices: projArcVertices.append(crsTransform.transform(vertex)) outFeat = QgsFeature() fields = self.layer.dataProvider().fields() outFeat.setAttributes([None] * fields.count()) outFeat.setGeometry( QgsGeometry.fromPolyline(projArcVertices)) self.layer.addFeature(outFeat) # trim segments p1 = crsTransform.transform(p1) feature1 = self.getFeature(self.segment1) geom1 = feature1.geometry() geom1.insertVertex(p1.x(), p1.y(), self.segment1.afterVertexNr) if revert1: geom1.deleteVertex(self.segment1.afterVertexNr + 1) # insertion above => +1 else: geom1.deleteVertex(self.segment1.beforeVertexNr) self.layer.changeGeometry(feature1.id(), geom1) p2 = crsTransform.transform(p2) feature2 = self.getFeature(self.segment2) geom2 = feature2.geometry() geom2.insertVertex(p2.x(), p2.y(), self.segment2.afterVertexNr) if revert2: geom2.deleteVertex(self.segment2.afterVertexNr + 1) # insertion above => +1 else: geom2.deleteVertex(self.segment2.beforeVertexNr) self.layer.changeGeometry(feature2.id(), geom2) self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "Success", QgsMessageBar.INFO, 5) self.layer.endEditCommand() else: self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "No arc was created", QgsMessageBar.WARNING, duration=5) self.layer.destroyEditCommand() except Exception as e: QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.removeAllMessages() self.messageBarUtils.showMessage( FilletDigitizingMode.MESSAGE_HEADER, "There was an error performing this command. See QGIS Message log for details.", QgsMessageBar.CRITICAL, duration=5) self.layer.destroyEditCommand() return finally: # select another fillet self.resetFillet() self.next() def extend(self, ip, snapSegment, extendUtils): featureExtend = self.getFeature(snapSegment) geometryExtend = featureExtend.geometry() bvExtend = geometryExtend.vertexAt(snapSegment.beforeVertexNr) avExtend = geometryExtend.vertexAt(snapSegment.afterVertexNr) number = extendUtils.vertexIndexToMove(bvExtend, avExtend, snapSegment, ip) if number == None: # not an extend return False else: fid = featureExtend.id() geometryExtend.moveVertex(ip.x(), ip.y(), number) self.layer.changeGeometry(fid, geometryExtend) return True def computeArcParameters(self, ip, snapSegment1, snapSegment2, filletRadius, extendUtils): bvExtend1 = snapSegment1.beforeVertex avExtend1 = snapSegment1.afterVertex # TODO => mustExtend =True and check the 2 segment have the same endpoint at ip (with some tolerance) iVToMove1 = extendUtils.vertexIndexToMove(bvExtend1, avExtend1, snapSegment1, ip, mustExtend=False) bvExtend2 = snapSegment2.beforeVertex avExtend2 = snapSegment2.afterVertex iVToMove2 = extendUtils.vertexIndexToMove(bvExtend2, avExtend2, snapSegment2, ip, mustExtend=False) qDebug(repr(iVToMove1) + " " + repr(iVToMove2)) if iVToMove1 == None or iVToMove2 == None: return (None, None, None, None, None) segment1 = [bvExtend1, avExtend1] segment2 = [bvExtend2, avExtend2] # determine intersection point (ip) and revert if needed revert1 = False revert2 = False if iVToMove1 == snapSegment1.beforeVertexNr: segment1[0] = ip else: revert1 = True segment1[1] = segment1[0] segment1[0] = ip if iVToMove2 == snapSegment2.beforeVertexNr: segment2[0] = ip else: revert2 = True segment2[1] = segment2[0] segment2[0] = ip lengthSegment1 = self.length(segment1) normedSegment1 = self.norm(segment1, lengthSegment1) lengthSegment2 = self.length(segment2) normedSegment2 = self.norm(segment2, lengthSegment2) midp1 = self.pointAtDist(normedSegment1, filletRadius) midp2 = self.pointAtDist(normedSegment2, filletRadius) # get point on bisector midp = QgsPoint((midp1.x() + midp2.x()) / 2.0, (midp1.y() + midp2.y()) / 2.0) # get angles along lines from intersection ang1 = self.angleFromX(ip, midp1) ang2 = self.angleFromX(ip, midp2) # get bisector angle ang = self.angleFromX(ip, midp) # get a half of angle between segments bis = abs(ang2 - ang1) / 2.0 # calculate hypotenuse (fillet draws slice of circle tangent to segments) hyp = filletRadius / math.sin(bis) # calculate another leg of a triangle cat = math.sqrt(hyp**2 - filletRadius**2) if cat > self.length(segment1) or cat > self.length(segment2): # radius is too big => nothing done return (None, None, None, None, None) # calculate point on arc pa = self.polarPoint(ip, ang, hyp - filletRadius) # calculate start point of arc p1 = self.polarPoint(ip, ang1, cat) # calculate end point of arc p2 = self.polarPoint(ip, ang2, cat) return (pa, p1, p2, revert1, revert2) def length(self, segment): return math.sqrt((segment[0].x() - segment[1].x())**2 + (segment[0].y() - segment[1].y())**2) def norm(self, segment, length): return [ segment[0], QgsPoint( segment[0].x() + (segment[1].x() - segment[0].x()) / length, segment[0].y() + (segment[1].y() - segment[0].y()) / length) ] def pointAtDist(self, normedSegment, dist): return QgsPoint( normedSegment[0].x() + dist * (normedSegment[1].x() - normedSegment[0].x()), normedSegment[0].y() + dist * (normedSegment[1].y() - normedSegment[0].y())) def polarPoint(self, basepoint, angle, distance): return QgsPoint(basepoint.x() + (distance * math.cos(angle)), basepoint.y() + (distance * math.sin(angle))) def angleFromX(self, pt1, pt2): dx = pt2.x() - pt1.x() dy = pt2.y() - pt1.y() ang = math.atan2(dy, dx) if ang < 0.0: return ang + 2.0 * math.pi else: return ang def getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = self.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def done(self): self.reset() self.iface.mapCanvas().unsetMapTool(self) def enter(self): #ignore pass def canvasPressEvent(self, e): if e.button() == Qt.RightButton: if self.step == 0: self.done() return if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e)
class ExtendDigitizingMode(QgsMapToolEmitPoint): def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandBoundary = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandBoundary.setColor(QColor(255, 0, 0)) self.rubberBandBoundary.setWidth(2) self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages=True): self.step = 0 self.snappingResultBoundaryEdge = None self.snappingResultExtendEdge = None self.cachedSpatialIndex = None self.rubberBandBoundary.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def resetExtend(self, full=False): if full: self.reset(False) else: # jsut the last step self.step = 1 self.snappingResultExtendEdge = None def deactivate(self): self.reset() QgsMapToolEmitPoint.deactivate(self) def next(self): if self.step == 0: # always starts at this step: test if there is a selection if self.layer.selectedFeatureCount() > 0: # next self.step = 1 self.next() return self.messageBarUtils.showButton("Extend", "Select boundary edge", "Use full layer", buttonCallback=self.useFullLayer) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.boundaryEdgeFound) elif self.step == 1: self.messageBarUtils.showButton("Extend", "Select edge to extend", "Done", buttonCallback=self.done) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.extendEdgeFound) elif self.step == 2: self.doExtend() def boundaryEdgeFound(self, result, pointClicked): self.snappingResultBoundaryEdge = result self.segmentFinderTool.segmentFound.disconnect(self.boundaryEdgeFound) self.segmentFinderTool.deactivate() self.rubberBandBoundary.reset(QGis.Line) self.rubberBandBoundary.addPoint(result.beforeVertex) self.rubberBandBoundary.addPoint(result.afterVertex, True) self.rubberBandBoundary.show() self.step = 1 self.next() def extendEdgeFound(self, result, pointClicked): self.snappingResultExtendEdge = result self.segmentFinderTool.segmentFound.disconnect(self.extendEdgeFound) self.segmentFinderTool.deactivate() transform = QgsCoordinateTransform( self.iface.mapCanvas().mapSettings().destinationCrs(), self.layer.crs()) g = QgsGeometry.fromPoint(pointClicked) g.transform(transform) self.pointExtend = g.asPoint() self.step = 2 self.next() def doExtend(self): self.messageBarUtils.showMessage("Extend", "Running...", duration=0) extendUtils = ExtendUtils(self.iface) try: featureExtend = self._getFeature(self.snappingResultExtendEdge) geometryExtend = featureExtend.geometry() bvExtend = geometryExtend.vertexAt( self.snappingResultExtendEdge.beforeVertexNr) avExtend = geometryExtend.vertexAt( self.snappingResultExtendEdge.afterVertexNr) vertexNrToMove = extendUtils.vertexIndexToMove( bvExtend, avExtend, self.snappingResultExtendEdge, self.pointExtend, mustExtend=False) vertexToMove = geometryExtend.vertexAt(vertexNrToMove) testRay = self.getTestRayForExtend(extendUtils, geometryExtend, bvExtend, avExtend, vertexNrToMove) # 3 cases: selection on layer, whole layer, cutting edge if self.layer.selectedFeatureCount( ) > 0 or not self.snappingResultBoundaryEdge: # use same strategy for both pointIntersection, alreadyNearEdge = self.getExtendInformationFromLayer( extendUtils, testRay, vertexToMove) else: # no ambiguity here pointIntersection, alreadyNearEdge = self.getExtendInformationFromSnappedBoundaryEdge( extendUtils, testRay, vertexToMove) if alreadyNearEdge: self.messageBarUtils.showMessage( "Extend", "Nothing done: Already at boundary edge", QgsMessageBar.WARNING, duration=2) elif pointIntersection: if self.cachedSpatialIndex: self.cachedSpatialIndex.deleteFeature(featureExtend) fid = featureExtend.id() geometryExtend.moveVertex(pointIntersection.x(), pointIntersection.y(), vertexNrToMove) self.layer.beginEditCommand("Extend") self.layer.changeGeometry(fid, geometryExtend) if self.cachedSpatialIndex: self.cachedSpatialIndex.insertFeature(featureExtend) self.layer.endEditCommand() self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage( "Extend", "The segment was extended successfully", duration=2) else: self.messageBarUtils.showMessage( "Extend", "Nothing done: No suitable boundary edge", QgsMessageBar.WARNING, duration=2) except Exception as e: QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.showMessage( "Trim", "There was an error performing this command. See QGIS Message log for details", QgsMessageBar.CRITICAL, duration=5) # select another edge to extend self.resetExtend() self.next() def _getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = self.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def buildSpatialIndex(self): if not self.cachedSpatialIndex: # Build the spatial index for faster lookup. index = QgsSpatialIndex() for f in vectorlayerutils.features(self.layer): index.insertFeature(f) self.cachedSpatialIndex = index def isNearEdgeInLayer(self, point, distance): bufferGeometry = QgsGeometry.fromPoint(point).buffer(distance, 2) fids = self.cachedSpatialIndex.intersects(bufferGeometry.boundingBox()) for fid in fids: if fid == self.snappingResultExtendEdge.snappedAtGeometry: continue feature = self.layer.getFeatures(QgsFeatureRequest(fid)).next() geometryTest = feature.geometry() if bufferGeometry.intersects(geometryTest): return True return False def getExtendInformationFromLayer(self, extendUtils, testRay, vertexToMove): self.buildSpatialIndex() if self.isNearEdgeInLayer(vertexToMove, constants.TOLERANCE_DEGREE): return (None, True) # get segment fids = self.cachedSpatialIndex.intersects(testRay.boundingBox()) candidates = [] for fid in fids: if fid == self.snappingResultExtendEdge.snappedAtGeometry: continue feature = self.layer.getFeatures(QgsFeatureRequest(fid)).next() geometryTest = feature.geometry() if testRay.intersects(geometryTest): candidates.extend( vectorlayerutils.segmentIntersectionPoint( geometryTest, testRay)) # extend as short as possible among the candidate intersection points if len(candidates) > 0: distancesToVertexToMove = map( lambda x: self.sqDistance(vertexToMove, x), candidates) closestIndices = sorted(range(len(distancesToVertexToMove)), key=lambda i: distancesToVertexToMove[i]) return (candidates[closestIndices[0]], False) return (None, False) def isNearSingleEdge(self, point, edge, distance): bufferGeometry = QgsGeometry.fromPoint(point).buffer(distance, 2) return edge.intersects(bufferGeometry) def getExtendInformationFromSnappedBoundaryEdge(self, extendUtils, testRay, vertexToMove): featureBoundary = self._getFeature(self.snappingResultBoundaryEdge) geometryBoundary = featureBoundary.geometry() bvBoundary = geometryBoundary.vertexAt( self.snappingResultBoundaryEdge.beforeVertexNr) avBoundary = geometryBoundary.vertexAt( self.snappingResultBoundaryEdge.afterVertexNr) edgeBoundary = QgsGeometry.fromPolyline([bvBoundary, avBoundary]) if self.isNearSingleEdge(vertexToMove, edgeBoundary, constants.TOLERANCE_DEGREE): return (None, True) if testRay.intersects(edgeBoundary): # only one possible intersection point (2 edges) pointIntersection = vectorlayerutils.segmentIntersectionPoint( testRay, edgeBoundary) if pointIntersection: pointIntersection = pointIntersection[0] return (pointIntersection, False) return (None, False) def getTestRayForExtend(self, extendUtils, geometryExtend, bvExtend, avExtend, vertexNrToMove): # Create a segment that goes away from the edgeExtend, starting at vertexNrToMove # and ending at the bounding box of the layer if vertexNrToMove == self.snappingResultExtendEdge.afterVertexNr: baseVertex = avExtend # direction towards which the extension will be performed vectorDirection = [bvExtend, avExtend] else: baseVertex = bvExtend vectorDirection = [avExtend, bvExtend] boundingBox = self.layer.extent() topLeft = QgsPoint(boundingBox.xMinimum(), boundingBox.yMaximum()) topRight = QgsPoint(boundingBox.xMaximum(), boundingBox.yMaximum()) bottomRight = QgsPoint(boundingBox.xMaximum(), boundingBox.yMinimum()) bottomLeft = QgsPoint(boundingBox.xMinimum(), boundingBox.yMinimum()) topIntersection = extendUtils.intersectionPoint( bvExtend, avExtend, topLeft, topRight) # check that the vector from baseVertex to that point is in the same direction as # vector direction if topIntersection and self.dp(vectorDirection[0], vectorDirection[1], baseVertex, topIntersection) > 0: return QgsGeometry.fromPolyline([baseVertex, topIntersection]) rightIntersection = extendUtils.intersectionPoint( bvExtend, avExtend, topRight, bottomRight) if rightIntersection and self.dp(vectorDirection[0], vectorDirection[1], baseVertex, rightIntersection) > 0: return QgsGeometry.fromPolyline([baseVertex, rightIntersection]) bottomIntersection = extendUtils.intersectionPoint( bvExtend, avExtend, bottomRight, bottomLeft) if bottomIntersection and self.dp(vectorDirection[0], vectorDirection[1], baseVertex, bottomIntersection) > 0: return QgsGeometry.fromPolyline([baseVertex, bottomIntersection]) leftIntersection = extendUtils.intersectionPoint( bvExtend, avExtend, bottomLeft, topLeft) if leftIntersection and self.dp(vectorDirection[0], vectorDirection[1], baseVertex, leftIntersection) > 0: return QgsGeometry.fromPolyline([baseVertex, leftIntersection]) def dp(self, pt11, pt12, pt21, pt22): return (pt12.x() - pt11.x()) * (pt22.x() - pt21.x()) + ( pt12.y() - pt11.y()) * (pt22.y() - pt21.y()) def sqDistance(self, pt1, pt2): return (pt1.x() - pt2.x())**2 + (pt1.y() - pt2.y())**2 def useFullLayer(self): self.snappingResultBoundaryEdge = None self.segmentFinderTool.segmentFound.disconnect(self.boundaryEdgeFound) self.segmentFinderTool.deactivate() self.step = 1 self.next() def done(self): self.reset() self.iface.mapCanvas().unsetMapTool(self) def enter(self): #ignore pass def canvasPressEvent(self, e): # use right button instead of clicking on button in message bar if e.button() == Qt.RightButton: if self.step == 0: self.useFullLayer() return elif self.step == 1: self.resetExtend(True) self.next() return if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e)
class DrawArcDigitizingMode(QgsMapToolEmitPoint): MESSAGE_HEADER = "Draw Arc" def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandBase = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandBase.setColor(QColor(255, 0, 0)) self.rubberBandBase.setWidth(2) self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages=True): self.step = 0 self.baseEdge = None self.rubberBandBase.reset(QGis.Line) try: self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def deactivate(self): self.reset() super(QgsMapToolEmitPoint, self).deactivate() def next(self): if self.step == 0: self.messageBarUtils.showMessage( DrawArcDigitizingMode.MESSAGE_HEADER, "Select base edge", duration=0) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.baseEdgeFound) elif self.step == 1: self.messageBarUtils.showMessage( DrawArcDigitizingMode.MESSAGE_HEADER, "Draw node for arc", duration=0) self.currentMapTool = None elif self.step == 2: self.doDrawArc() def baseEdgeFound(self, result, pointClicked): self.baseEdge = result self.segmentFinderTool.segmentFound.disconnect(self.baseEdgeFound) self.segmentFinderTool.deactivate() self.rubberBandBase.reset(QGis.Line) self.rubberBandBase.addPoint(result.beforeVertex) self.rubberBandBase.addPoint(result.afterVertex, True) self.rubberBandBase.show() self.step = 1 self.next() def doDrawArc(self): self.messageBarUtils.showMessage(DrawArcDigitizingMode.MESSAGE_HEADER, "Running...", duration=0) try: # get the actual vertices from the geometry instead of the snapping # result vertices which are in map canvas CRS featureBase = self._getFeature(self.baseEdge) geometryBase = featureBase.geometry() settings = QSettings() method = settings.value(constants.ARC_METHOD, "") if method == constants.ARC_METHOD_NUMBEROFPOINTS: value = settings.value(constants.ARC_NUMBEROFPOINTS, 0, type=int) else: value = settings.value(constants.ARC_ANGLE, 0, type=float) # arc done in map canvas coordinate g = CircularArc.getInterpolatedArc(self.baseEdge.beforeVertex, self.arcNode, self.baseEdge.afterVertex, method, value) arcVertices = g.asPolyline() if len(arcVertices) > 2: # to project the vertices of the arc in layer crs crsDest = self.layer.crs() canvas = self.iface.mapCanvas() mapRenderer = canvas.mapSettings() crsSrc = mapRenderer.destinationCrs() crsTransform = QgsCoordinateTransform(crsSrc, crsDest) # pass over first and last vertex (bvBase and avBase) for i in range(len(arcVertices) - 2, 0, -1): arcVertex = arcVertices[i] arcVertex = crsTransform.transform(arcVertex) geometryBase.insertVertex(arcVertex.x(), arcVertex.y(), self.baseEdge.afterVertexNr) # the newly inserted vertex has the index of the original avBase so all good self.layer.beginEditCommand( DrawArcDigitizingMode.MESSAGE_HEADER) self.layer.changeGeometry(featureBase.id(), geometryBase) self.layer.endEditCommand() self.messageBarUtils.showMessage( DrawArcDigitizingMode.MESSAGE_HEADER, "Success", QgsMessageBar.INFO, duration=5) else: self.messageBarUtils.showMessage( DrawArcDigitizingMode.MESSAGE_HEADER, "No arc was created", QgsMessageBar.WARNING, duration=5) except Exception as e: self.reset() QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.showMessage( DrawArcDigitizingMode.MESSAGE_HEADER, "There was an error performing this command. See QGIS Message log for details", QgsMessageBar.CRITICAL, duration=5) finally: self.iface.mapCanvas().refresh() self.iface.mapCanvas().unsetMapTool(self) def _getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = self.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def enter(self): #ignore pass def canvasPressEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) elif self.step == 1: # just point selection self.arcNode = self.toMapCoordinates(e.pos()) self.step = 2 self.next() def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e)
class TrimDigitizingMode(QgsMapToolEmitPoint): def __init__(self, iface): self.iface = iface QgsMapToolEmitPoint.__init__(self, self.iface.mapCanvas()) self.messageBarUtils = MessageBarUtils(iface) self.segmentFinderTool = SegmentFinderTool(self.iface.mapCanvas()) self.rubberBandCutting = QgsRubberBand(self.iface.mapCanvas()) self.rubberBandCutting.setColor(QColor(255, 0, 0)) self.rubberBandCutting.setWidth(2) self.layer = None self.reset() def setLayer(self, layer): self.layer = layer def reset(self, clearMessages = True): self.step = 0 self.snappingResultCuttingEdge = None self.snappingResultTrim = None self.cachedSpatialIndex = None self.rubberBandCutting.reset(QGis.Line) try:self.segmentFinderTool.segmentFound.disconnect() except Exception: pass if clearMessages: self.messageBarUtils.removeAllMessages() def resetTrim(self): self.step = 1 self.snappingResultTrim = None def deactivate(self): self.reset() QgsMapToolEmitPoint.deactivate(self) def next(self): if self.step == 0: # alwways starts at this step: test if there is a selection if self.layer.selectedFeatureCount() > 0: # next self.step = 1 self.next() return self.messageBarUtils.showButton("Trim", "Select cutting edge", "Use full layer", buttonCallback=self.useFullLayer) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.cuttingEdgeFound) elif self.step == 1: self.messageBarUtils.showButton("Trim", "Select segment to trim","Done",buttonCallback=self.done) self.currentMapTool = self.segmentFinderTool self.segmentFinderTool.segmentFound.connect(self.trimEdgeFound) elif self.step == 2: self.doTrim() def cuttingEdgeFound(self, result, pointClicked): self.snappingResultCuttingEdge = result self.segmentFinderTool.segmentFound.disconnect(self.cuttingEdgeFound) self.segmentFinderTool.deactivate() self.rubberBandCutting.reset(QGis.Line) self.rubberBandCutting.addPoint(result.beforeVertex) self.rubberBandCutting.addPoint(result.afterVertex, True) self.rubberBandCutting.show() self.step = 1 self.next() def trimEdgeFound(self, result, pointClicked): self.snappingResultTrim = result self.segmentFinderTool.segmentFound.disconnect(self.trimEdgeFound) self.segmentFinderTool.deactivate() transform = QgsCoordinateTransform(self.iface.mapCanvas().mapSettings().destinationCrs(), self.layer.crs()) g = QgsGeometry.fromPoint(pointClicked) g.transform(transform) self.pointTrim = g.asPoint() self.step = 2 self.next() def doTrim(self): self.messageBarUtils.showMessage("Trim", "Running...", duration=0) trimUtils = TrimUtils(self.iface) try: qDebug("New Trim") # 3 cases: selection on layer, whole layer, cutting edge if self.layer.selectedFeatureCount() > 0 or not self.snappingResultCuttingEdge: # use same strategy for both indexReferenceIntersection, pointIntersections = self.getTrimmingInformationFromLayer(trimUtils) else: # no ambiguity here indexReferenceIntersection, pointIntersections = self.getTrimmingInformationFromSnappedGeometry(trimUtils) if indexReferenceIntersection != None: featureTrim = self._getFeature(self.snappingResultTrim) geometryTrim = featureTrim.geometry() parts, unconcernedParts = trimUtils.removeVertices(geometryTrim, indexReferenceIntersection, pointIntersections, self.snappingResultTrim.afterVertexNr, self.pointTrim) parts = self.cleanupTrimmedParts(parts) if len(parts) > 0: if unconcernedParts: parts.extend(unconcernedParts) # replace geometry self.layer.beginEditCommand("Trim") # geometry updated => remove from index (readded very soon) if self.cachedSpatialIndex: self.cachedSpatialIndex.deleteFeature(featureTrim) # by default create a multipart geometry (unless not supported by data provider) # for ex PostGIS layer can be constrained to be multipart # some data types can have mixed multi/single (ex shp) if (geometryTrim.isMultipart() or len(parts) > 1) and QGis.isMultiType(self.layer.dataProvider().geometryType()) : qDebug("Multipart") geometryTrim = QgsGeometry.fromMultiPolyline(map(lambda x:x.asPolyline(),parts)) self.layer.changeGeometry(featureTrim.id(), geometryTrim) # for some reason, the line above makes it so the feature cannot be deleted from the index the next time featureTrim.setGeometry(geometryTrim) if self.cachedSpatialIndex: self.cachedSpatialIndex.insertFeature(featureTrim) else: qDebug("Singlepart") self.layer.deleteFeature(featureTrim.id()) # create a new feature for each part (so still single part geometries) for part in parts: featureTrim.setGeometry(part) self.layer.addFeature(featureTrim) if self.cachedSpatialIndex: self.cachedSpatialIndex.insertFeature(featureTrim) self.layer.endEditCommand() self.iface.mapCanvas().refresh() self.messageBarUtils.showMessage("Trim", "The segment was trimmed successfully", duration=2) else: self.messageBarUtils.showMessage("Trim", "Nothing done: Trimming would create an invalid geometry", QgsMessageBar.WARNING, duration=2) else: self.messageBarUtils.showMessage("Trim", "Nothing done: The geometries are not intersecting", QgsMessageBar.WARNING, duration=2) except Exception as e: QgsMessageLog.logMessage(repr(e)) self.messageBarUtils.showMessage("Trim", "There was an error performing this command. See QGIS Message log for details", QgsMessageBar.CRITICAL, duration=5) self.layer.destroyEditCommand() self.done() self.resetTrim() self.next() def cleanupTrimmedParts(self, parts): return filter(lambda x: x and x.length() > constants.TOLERANCE_DEGREE*2, parts) def _getFeature(self, snappingResult): fid = snappingResult.snappedAtGeometry feature = QgsFeature() fiter = self.layer.getFeatures(QgsFeatureRequest(fid)) if fiter.nextFeature(feature): return feature return None def _getSegment(self, snappingResult): feature = self._getFeature(snappingResult) geometry = feature.geometry() bv = geometry.vertexAt(snappingResult.beforeVertexNr) av = geometry.vertexAt(snappingResult.afterVertexNr) return QgsGeometry.fromPolyline([bv,av]) def getTrimmingInformationFromSnappedGeometry(self, trimUtils): edgeCuttingEdge = self._getSegment(self.snappingResultCuttingEdge) featureTrim = self._getFeature(self.snappingResultTrim) geometryTrim = featureTrim.geometry() bufferGeometryTrim = geometryTrim.buffer(constants.TOLERANCE_DEGREE, 2) if bufferGeometryTrim.intersects(edgeCuttingEdge): candidates = trimUtils.intersectionPointWithTolerance(edgeCuttingEdge, bufferGeometryTrim, geometryTrim) if len(candidates) > 0: index = self.indexOfClosestIntersection(candidates) return index, candidates return (None, None) def buildSpatialIndex(self): if not self.cachedSpatialIndex: # Build the spatial index for faster lookup. index = QgsSpatialIndex() for f in vectorlayerutils.features(self.layer): index.insertFeature(f) self.cachedSpatialIndex = index def getTrimmingInformationFromLayer(self, trimUtils): try: self.buildSpatialIndex() # get geometry featureTrim = self._getFeature(self.snappingResultTrim) geometryTrim = featureTrim.geometry() # use buffered geometry to solve unexact geoemtries because of limited floating # precision (point on a line never exactly on the line) # (sometimes no intersection is present when there should be) bufferGeometryTrim = geometryTrim.buffer(constants.TOLERANCE_DEGREE, 2) fids = self.cachedSpatialIndex.intersects(bufferGeometryTrim.boundingBox()) if len(fids) > 0: candidates = [] for fid in fids: if fid == self.snappingResultTrim.snappedAtGeometry: continue features = self.layer.getFeatures(QgsFeatureRequest(fid)) feature = features.next() geometryTest = feature.geometry() if bufferGeometryTrim.intersects(geometryTest): pointIntersection = trimUtils.intersectionPointWithTolerance(geometryTest, bufferGeometryTrim, geometryTrim) candidates.extend(pointIntersection) if len(candidates) > 0: index = self.indexOfClosestIntersection(candidates) return index, candidates return (None, None) except StopIteration: # index is invalid maybe because undo was performed: force rebuild # and relaunch self.cachedSpatialIndex = None return self.getTrimmingInformationFromLayer(trimUtils) def indexOfClosestIntersection(self, candidates): distancesToSnappedTrimPoint = map(lambda x: sqDistance(self.pointTrim, x), candidates) closestIndices = sorted(range(len(distancesToSnappedTrimPoint)),key=lambda i:distancesToSnappedTrimPoint[i]) return closestIndices[0] def useFullLayer(self): self.snappingResultCuttingEdge = None # cleanup self.segmentFinderTool.segmentFound.disconnect(self.cuttingEdgeFound) self.segmentFinderTool.deactivate() self.step = 1 self.next() def done(self): self.reset() self.iface.mapCanvas().unsetMapTool(self) def enter(self): #ignore pass def canvasPressEvent(self, e): # use right button instead of clicking on button in message bar if e.button() == Qt.RightButton: if self.step == 0: self.useFullLayer() return elif self.step == 1: self.done() return if self.currentMapTool: self.currentMapTool.canvasPressEvent(e) def canvasReleaseEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasReleaseEvent(e) def canvasMoveEvent(self, e): if self.currentMapTool: self.currentMapTool.canvasMoveEvent(e)