PyQGIS オブジェクトを移動、拡大縮小、回転させるサンプル

ここで紹介しているサンプルはオブジェクトの形状を変更するものなので、実行する際はバックアップを必ず取ってください。


どんな動きをするの?

f:id:Chiakikun:20200621144611p:plain

レイヤをアクティブにして、プラグインを実行します。オブジェクトをクリックすると...

 

f:id:Chiakikun:20200621144802p:plain

オブジェクトが編集状態になり、移動、拡大縮小、回転ができるようになります。上の画像では、マルチポリゴンの一部を回転させています。

 

f:id:Chiakikun:20200621144820p:plain

回転できました。

 

f:id:Chiakikun:20200621144836p:plain

ライン(シングルオブジェクト)を拡大しています。

 

f:id:Chiakikun:20200621144852p:plain

マルチポリゴンの一部を移動させています。

注意 このサンプルではドーナツのポリゴンには対応していません。


コード

import qgis
from qgis.core import *
from qgis.gui  import *
from PyQt5.Qt import pyqtSignal
from qgis.PyQt.QtGui import QColor, QTransform
import math


class EditGeometrySample(QgsMapToolIdentifyFeature):
    setFeature = pyqtSignal(QgsGeometry)

    def __init__(self, iface, canvas, edittype): # edittype = 'scale', 'move', 'rotate'
        self.iface = iface
        self.canvas = canvas
        self.layer = self.iface.activeLayer()
        self.objType = self.layer.geometryType()
        self.edittype = edittype

        QgsMapToolIdentifyFeature.__init__(self, self.canvas)
        self.myRubberBand = None


    def canvasPressEvent(self, event): # featureIdentifiedより先に呼ばれる
        self.srcpos = event.pos()

        if self.myRubberBand == None: return

        if event.button() == QtCore.Qt.LeftButton:
            # self.layer.startEditing() # バックアップは取りましたか?取らずに実行しても責任は取れませんよ?バックアップできたなら、#を外してください
            self.changeGeometry(self.layer.selectedFeatures()[0])
            # self.layer.commitChanges() # ここも

        self.myRubberBand.reset()
        self.myRubberBand = None
        self.layer.removeSelection() 


    def setFeature(self, feat):
        if self.myRubberBand != None: return

        self.layer.select(feat.id())

        curpos = QgsGeometry().fromPointXY(self.toMapCoordinates(self.srcpos))

        mpol = self.tolist(feat.geometry())

        # 選択したマルチオブジェクトで、選択位置に一番近いオブジェクト取得
        dist = [pol.distance(curpos) for pol in mpol]
        self.nearidx = dist.index(min(dist))
        self.nearobj = mpol[self.nearidx]

        # ラバーバンドにnearobjを追加する
        self.myRubberBand = QgsRubberBand( self.canvas, self.objType )
        self.myRubberBand.setColor(QColor(255, 0, 0, 255))
        self.myRubberBand.addGeometry(self.nearobj, self.layer)


    def canvasMoveEvent(self, e):
        if self.myRubberBand == None: return

        self.myRubberBand.reset(self.objType)

        dstpos = e.pos()

        if self.edittype == 'move':
            self.moveObject(self.srcpos, dstpos)
        elif self.edittype == 'scale':
            self.scaleObject(self.srcpos, dstpos)
        else:
            self.rotateObject(self.srcpos, dstpos)


    def changeGeometry(self, feat):

        if self.objType == QgsWkbTypes.GeometryType.PolygonGeometry:
            if feat.geometry().isMultipart():
                mpol = feat.geometry().asMultiPolygon()
                mpol[self.nearidx] = self.myRubberBand.asGeometry().asPolygon()
                self.layer.changeGeometry (feat.id(), QgsGeometry().fromMultiPolygonXY(mpol))
            else:
                pol = self.myRubberBand.asGeometry().asPolygon()
                self.layer.changeGeometry (feat.id(), QgsGeometry.fromPolygonXY(pol))

        elif self.objType == QgsWkbTypes.GeometryType.LineGeometry:
            if feat.geometry().isMultipart():
                mline = feat.geometry().asMultiPolyline()
                mline[self.nearidx] = self.myRubberBand.asGeometry().asPolyline()
                self.layer.changeGeometry (feat.id(), QgsGeometry().fromMultiPolylineXY(mline))
            else:
                line = self.myRubberBand.asGeometry().asPolyline()
                self.layer.changeGeometry (feat.id(), QgsGeometry.fromPolylineXY(line))

        elif self.objType == QgsWkbTypes.GeometryType.PointGeometry:
            if feat.geometry().isMultipart():
                mpnt = feat.geometry().asMultiPoint()
                mpnt[self.nearidx] = self.myRubberBand.asGeometry().asMultiPoint()[0]
                self.layer.changeGeometry (feat.id(), QgsGeometry().fromMultiPointXY(mpnt))
            else:
                pnt = self.myRubberBand.asGeometry().asMultiPoint()[0]
                self.layer.changeGeometry (feat.id(), QgsGeometry.fromPointXY(pnt))


    def tolist(self, obj):

        if self.objType == QgsWkbTypes.GeometryType.PolygonGeometry:
            if obj.isMultipart():
                return [QgsGeometry().fromPolygonXY(pol) for pol in obj.asMultiPolygon()]
            else:
                return [QgsGeometry().fromPolygonXY(obj.asPolygon())]

        elif self.objType == QgsWkbTypes.GeometryType.LineGeometry:
            if obj.isMultipart():
                return [QgsGeometry().fromPolylineXY(pline) for pline in obj.asMultiPolyline()]
            else:
                return [QgsGeometry().fromPolylineXY(obj.asPolyline())]

        elif self.objType == QgsWkbTypes.GeometryType.PointGeometry:
            if obj.isMultipart():
                return [QgsGeometry().fromPointXY(pnt) for pnt in obj.asMultiPoint()]
            else:
                return [QgsGeometry().fromPointXY(obj.asPoint())]


    def moveObject(self, srcpos, dstpos):
        temp = QgsGeometry(self.nearobj)

        s = self.toMapCoordinates(srcpos)
        d = self.toMapCoordinates(dstpos)

        self.myRubberBand.addGeometry(self.trans(temp, s, d), self.layer)


    def scaleObject(self, srcpos, dstpos):
        original = QgsPointXY(0, 0)
        centroid = self.nearobj.centroid().asPoint()
        temp = QgsGeometry(self.nearobj)

        s = QgsGeometry().fromPointXY(QgsPointXY(srcpos))
        d = QgsGeometry().fromPointXY(QgsPointXY(dstpos))

        # 一度中心に持ってこないとオブジェクトが変な方向に行ってしまうので
        temp = self.trans(temp, centroid, original)
        temp = self.scale(temp, s, d)
        temp = self.trans(temp, original, centroid)

        self.myRubberBand.addGeometry(temp, self.layer)


    def rotateObject(self, srcpos, dstpos):
        original = QgsPointXY(0, 0)
        centroid = self.nearobj.centroid().asPoint()
        temp = QgsGeometry(self.nearobj)

        s = QgsPointXY(self.toMapCoordinates(srcpos))
        d = QgsPointXY(self.toMapCoordinates(dstpos))

        # 一度中心に持ってこないとオブジェクトが変な方向に行ってしまうので
        temp = self.trans(temp, centroid, original)
        temp = self.rotate(temp, s, d)
        temp = self.trans(temp, original, centroid)

        self.myRubberBand.addGeometry(temp, self.layer)


    def trans(self, obj, srcpos, dstpos):
        x = dstpos.x() - srcpos.x()
        y = dstpos.y() - srcpos.y()

        obj.transform(QTransform(1, 0, 0, 1, x, y))
        return obj


    def scale(self, obj, srcpos, dstpos):
        cent = QgsGeometry().fromPointXY(QgsPointXY(0, 0))
        srclen = cent.distance(srcpos)
        dstlen = cent.distance(dstpos)
        ratio = (dstlen / srclen) * 1.5 # 「1.5」は適当

        obj.transform(QTransform(ratio, 0, 0, ratio, 0, 0))
        return obj


    def rotate(self, obj, srcpos, dstpos):
        centroid = self.nearobj.centroid().asPoint()
        theta = self.radian(centroid, dstpos)
        sin = math.sin(theta)
        cos = math.cos(theta)

        obj.transform(QTransform(cos, sin, -sin, cos, 0, 0))
        return obj


    def radian(self, a, b):
        return math.atan2(b.y() - a.y(), b.x() - a.x())


    def deactivate(self):
        try: # 左クリックで既にNoneしていた場合エラーになるので
            self.myRubberBand.reset()
        except:
            pass
        self.myRubberBand = None
        self.layer.removeSelection() 

ソースはこちら


使い方

例として、ここからダウンロードできるサンプルプラグインに追加します。

nodialog_skelton.pyと同じフォルダにソースを置いて、インポート部分に次のコードを追記します。

from .editgeometrysample import EditGeometrySample

nodialog_skelton.pyのstartを次のコードに書き換えます。

    def start(self):
        self.maptool = EditGeometrySample(self.iface, self.canvas, 'move') # 移動の場合
        self.maptool.setLayer(self.iface.activeLayer())
        self.maptool.featureIdentified.connect(self.selectedFeature)
        
        self.canvas.setMapTool(self.maptool)
        self.canvas.mapToolSet.connect(self.unsetTool) # このサンプル実行中に他のアイコンを押した場合

次のメソッドを追加してください。

    def selectedFeature(self, feature):        
        self.maptool.setFeature(feature)

このようになればOKです。

~略~

from .editgeometrysample import EditGeometrySample


class NodialogSkelton(qgis.gui.QgsMapTool):

    def selectedFeature(self, feature):        
        self.maptool.setFeature(feature)


    def start(self):
        self.maptool = EditGeometrySample(self.iface, self.canvas, 'move')
        self.maptool.setLayer(self.iface.activeLayer())
        self.maptool.featureIdentified.connect(self.selectedFeature)

        self.canvas.setMapTool(self.maptool)
        self.canvas.mapToolSet.connect(self.unsetTool) # このサンプル実行中に他のアイコンを押した場合
~略~

最後までご覧頂き、ありがとうございました

PyQGIS ラスタから値を取得するサンプル


どんな動きをするの?

f:id:Chiakikun:20200617221955p:plain

ラスタレイヤをアクティブにした状態でプラグインを実行すると、マウスカーソル位置の標高をPythonコンソールに表示します。


コード

import qgis
from qgis.core import *
from qgis.gui  import *
import pyproj

class GetRasterPixelValue:

    def __init__(self, layer: QgsRasterLayer):
        self.__grs80 = pyproj.Geod(ellps='GRS80')

        # ピクセル1つのサイズ
        self.__unitX = layer.rasterUnitsPerPixelX()
        self.__unitY = layer.rasterUnitsPerPixelY()

        # ラスタのサイズ
        self.__height = layer.height()
        self.__width = layer.width()

        # ラスタの原点
        self.__provider = layer.dataProvider()
        self.__extent = self.__provider.extent()
        self.__orgX = self.__extent.xMinimum()
        self.__orgY = self.__extent.yMaximum()

        # ピクセル
        self.block = self.__provider.block(1, self.__extent, self.__width, self.__height)


    def getIndex(self, pos: QgsPointXY):

        pixelIdxX = (pos.x() - self.__orgX) / self.__unitX
        pixelIdxY = (self.__orgY - pos.y()) / self.__unitY
        if (pixelIdxX < 0) or (pixelIdxY < 0):
            return None
        pixelIdxX = int(pixelIdxX)
        pixelIdxY = int(pixelIdxY)
        if (pixelIdxX > self.__width - 1) or (pixelIdxY > self.__height - 1):
            return None

        return [pixelIdxX, pixelIdxY]


    def getGeometry(self, idxX: int, idxY: int):
        if (idxX < 0) or (idxY < 0):
            return None
        if (idxX > self.__width - 1) or (idxY > self.__height - 1):
            return None

        pixelPosX = self.__unitX * idxX + self.__orgX + self.__unitX / 2
        pixelPosY = self.__orgY - self.__unitY * idxY - self.__unitY / 2
        return [pixelPosX, pixelPosY]


    def getValueFromPos(self, pos: QgsPointXY):
        ident = self.__provider.identify(pos, QgsRaster.IdentifyFormatValue )

        if ident.isValid():
            return list(ident.results().values())[0]
        else:
            return None


    def getValueFromIdx(self, idxX: int, idxY: int):
        if (idxX < 0) or (idxY < 0):
            return None
        if (idxX > self.__width - 1) or (idxY > self.__height - 1):
            return None

        return self.block.value(idxY, idxX)


    def getValueInterpolation(self, pos: QgsPointXY):

        pixidx = self.getIndex(pos)
        if pixidx == None:
            return None
        pixelIdxX = pixidx[0]
        pixelIdxY = pixidx[1]

        pixpos = self.getGeometry(pixelIdxX, pixelIdxY)
        pixelPosX = pixpos[0]
        pixelPosY = pixpos[1]

        # 右上
        if (pixelPosX < pos.x()) and (pixelPosY < pos.y()):
            if (pixelIdxX > self.__width - 2) or (pixelIdxY - 1 < 0):
                return None
            a = self.__getXYZ__(pixelIdxX+1, pixelIdxY-1)
            b = self.__getXYZ__(pixelIdxX,   pixelIdxY-1)
            c = self.__getXYZ__(pixelIdxX,   pixelIdxY)
            d = self.__getXYZ__(pixelIdxX+1, pixelIdxY)
        # 左上
        elif (pixelPosX >= pos.x()) and (pixelPosY < pos.y()):
            if (pixelIdxX - 1 < 0) or (pixelIdxY - 1 < 0):
                return None
            a = self.__getXYZ__(pixelIdxX,   pixelIdxY-1)
            b = self.__getXYZ__(pixelIdxX-1, pixelIdxY-1)
            c = self.__getXYZ__(pixelIdxX-1, pixelIdxY)
            d = self.__getXYZ__(pixelIdxX,   pixelIdxY)
        # 左下
        elif (pixelPosX >= pos.x()) and (pixelPosY >= pos.y()):
            if (pixelIdxX - 1 < 0) or (pixelIdxY > self.__height - 2):
                return None
            a = self.__getXYZ__(pixelIdxX,   pixelIdxY)
            b = self.__getXYZ__(pixelIdxX-1, pixelIdxY)
            c = self.__getXYZ__(pixelIdxX-1, pixelIdxY+1)
            d = self.__getXYZ__(pixelIdxX,   pixelIdxY+1)
        # 右下
        elif (pixelPosX < pos.x()) and (pixelPosY >= pos.y()):
            if (pixelIdxX > self.__width - 2) or (pixelIdxY > self.__height - 2):
                return None
            a = self.__getXYZ__(pixelIdxX+1, pixelIdxY)
            b = self.__getXYZ__(pixelIdxX,   pixelIdxY)
            c = self.__getXYZ__(pixelIdxX,   pixelIdxY+1)
            d = self.__getXYZ__(pixelIdxX+1, pixelIdxY+1)

        dist1 = self.__grs80.inv(b.x(), b.y(), a.x(), a.y())[2]
        dist2 = self.__grs80.inv(b.x(), b.y(), pos.x(), b.y())[2]
        delta = (a.z() - b.z()) / dist1
        tmp1 = delta * dist2 + b.z()

        dist1 = self.__grs80.inv(c.x(), c.y(), d.x(), d.y())[2]
        dist2 = self.__grs80.inv(c.x(), c.y(), pos.x(), c.y())[2]
        delta = (d.z() - c.z()) / dist1
        tmp2 = delta * dist2 + c.z()

        dist1 = self.__grs80.inv(d.x(), d.y(), a.x(), a.y())[2]
        dist2 = self.__grs80.inv(d.x(), d.y(), d.x(), pos.y())[2]
        delta = (tmp1 - tmp2) / dist1
        inp = delta * dist2 + tmp2

        return inp


    def __getXYZ__(self, idxX: int, idxY: int):
        x   = self.__unitX * idxX + self.__orgX + self.__unitX / 2
        y   = self.__orgY - self.__unitY * idxY - self.__unitY / 2

        z = self.block.value(idxY, idxX)

        return QgsPoint(x, y, z)
    

ソースはこちら


使い方

例として、マウスイベントサンプルに追加します。このmouseeventsample.pyの使い方はこちらを参照してください。

chiakikun.hatenadiary.com

mouseeventsample.pyとgetrasterpixelvalue.pyを同じフォルダに置いて、mouseeventsample.pyのインポート部分に次のコードを追記します。

from .getrasterpixelvalue import GetRasterPixelValue

mouseeventsample.pyの__init__の引数は次のように変更してください。

__init__(self, iface, canvas)

mouseeventsample.pyの__init__に次のコードを加えます。

self.gr = GetRasterPixelValue(iface.activeLayer())

mouseeventsample.pyのcanvasMoveEventを次のコードに変えます。

def canvasMoveEvent(self, event):
    print(str(self.gr.getValueInterpolation(self.toMapCoordinates(event.pos()))))

nodialog_skelton.pyのstartを次のコードに書き換えます。

    def start(self):
        maptool = MouseEventSample(self.iface, self.canvas)

nodialog_skelton.pyはこのようになればOKです。

~略~

from .mouseeventsample import MouseEventSample

class NodialogSkelton(qgis.gui.QgsMapTool):

    def start(self):
        maptool = MouseEventSample(self.iface, self.canvas)

        self.canvas.setMapTool(maptool)
        self.canvas.mapToolSet.connect(self.unsetTool) # このサンプル実行中に他のアイコンを押した場合
~略~

 

mouseeventsample.pyはこのようになればOKです。

~略~
from .getrasterpixelvalue import GetRasterPixelValue

class MouseEventSample(QgsMapTool):

    def __init__(self, iface, canvas):
        QgsMapTool.__init__(self, canvas)

        self.gr = GetRasterPixelValue(iface.activeLayer())


    def canvasMoveEvent(self, event):
        print(str(self.gr.getValueInterpolation(self.toMapCoordinates(event.pos()))))
~略~

最後までご覧頂き、ありがとうございました

PyQGIS リレーションのサンプル


どんな動きをするの?

このサンプルでは、国土数値情報ダウンロードサービスからダウンロードできる行政区域避難所を使います。

f:id:Chiakikun:20200606202451p:plain

プラグインを実行すると、上のようにリレーションが作られます。

 

f:id:Chiakikun:20200606202506p:plain

プラグインを実行したままで、行政区域の地物を選択すると...

 

f:id:Chiakikun:20200606202518p:plain

選択した地物と関係ある避難所の地物が選択状態になり、属性テーブルが表示されます。


コード

from qgis.PyQt import QtWidgets
from qgis.PyQt.Qt import QSettings
import qgis
from qgis.core import *
from qgis.gui  import *

class RelationSample:

    def __init__(self, iface, parentLayer, parentField, childLayer, childField):

        self.iface = iface

        self.rel = QgsRelation()
        self.rel.setReferencingLayer(childLayer.id())
        self.rel.setReferencedLayer(parentLayer.id())
        self.rel.addFieldPair(childField, parentField)
        self.rel.setId('適当なID')
        self.rel.setName('適当な名前')
        QgsProject.instance().relationManager().addRelation(self.rel)

        self.parent = parentLayer
        self.parent.selectionChanged.connect(self.showChildren)


    def showChildren(self):
        parent = self.rel.referencedLayer()
        child = self.rel.referencingLayer()

        features = parent.selectedFeatures()
        if len(features) == 0:
            return

        child.removeSelection() # クリアしないと、属性テーブルに余計に表示されるから 
        for c in self.rel.getRelatedFeatures(features[0]):
            child.select(c.id())

        selectedlayer = self.iface.activeLayer() # 退避
        try:
            # このプログラム実行中は属性テーブルは選択中のフューチャーしか表示しない
            self.oldsetting = QSettings().value('/Qgis/attributeTableBehaviour')
            QSettings().setValue('/Qgis/attributeTableBehavior', 'ShowSelected')

            self.iface.setActiveLayer(child)
            self.iface.mainWindow().findChild(QtWidgets.QAction, 'mActionOpenTable' ).trigger()
        finally:
            self.iface.setActiveLayer(selectedlayer) # 戻す
            QSettings().setValue('/Qgis/attributeTableBehavior', self.oldsetting)


    def __del__(self):
        QgsProject.instance().relationManager().removeRelation(self.rel)
        self.parent.selectionChanged.disconnect(self.showChildren)

ソースはこちら


使い方

例として、ここからダウンロードできるサンプルプラグインに追加します。

nodialog_skelton.pyと同じフォルダにソースを置いて、インポート部分に次のコードを追記します。

from .relationsample import RelationSample

nodialog_skelton.pyのstartを次のコードに書き換えます。

    def start(self):
        # 国土数値情報ダウンロードサービスからダウンロードできる行政区域と避難所を使う場合
        parent = QgsProject.instance().mapLayersByName('行政区域')[0]
        child =QgsProject.instance().mapLayersByName('避難所')[0]
        self.rel = RelationSample(self.iface, parent, 'N03_007', child, 'p20_001')

nodialog_skelton.pyのfinishを次のコードに書き換えます。

    def finish(self):
        self.rel = None

このようになればOKです。

~略~
from .relationsample import RelationSample

class NodialogSkelton(qgis.gui.QgsMapTool):

    def start(self):
        parent = QgsProject.instance().mapLayersByName('行政区域')[0]
        child =QgsProject.instance().mapLayersByName('避難所')[0]
        self.rel = RelationSample(self.iface, parent, 'N03_007', child, 'p20_001')
        
        
    def finish(self):
        self.rel = None
~略~

 


最後までご覧頂き、ありがとうございました