Edited at

QGIS3でpythonプラグインを作ってみた その5 地図をクリックして地物を選択する


QGIS3でpythonプラグインを作る その5


目標

前回作ったプラグインに、地図(マップキャンバス)をクリックして地物の選択ができる機能を追加する。

押さえたいところは


  • マップキャンバスのイベントを取得する方法

  • クリックイベントから座標を取得する方法

  • 座標から対象レイヤの地物を検索する方法

  • 一番近い点だけを選択状態にする方法


下準備


  • ポイントのシェープファイルがレイヤ登録されたQGISプロジェクトを用意。ダウンロードはこちら

  • 前回作ったプラグインを有効にしておく。GitHubのリポジトリはこちら

  • QGISのPluginReloaderプラグインを有効にしておく(任意)


GUI編集

QGIS付属の「Qt Designer with QGIS 3.0.0 custom widgets」を使ってGUIを編集します。

目標だけならボタン1個でいいのですが属性を表示するエディットラインも付けておきます。

ui2.png

物理名
論理名
備考

btn1
地物カウントボタン

その2で使用
アクティブレイヤの地物数を表示

cmb
レイヤコンボ

その3で使用
選択したレイヤの地物数を表示

lineEditLat
緯度入力

その4で使用
ポイント追加する緯度を入力

lineEditLon
経度入力

その4で使用
ポイント追加する経度を入力

btnAdd
ポイント追加ボタン

その4で使用
ポイントを追加

btnMove
ポイント移動ボタン

その4で使用
ポイントを移動

btnDel
ポイント削除ボタン

その4で使用
ポイントを削除

btnCanvasEvent
キャンバスイベント切替ボタン
マップキャンバスイベントの切替
checkableをTrueに設定してトグルボタンとして使用

lineEditId
id入力
地物属性idを表示
readOnlyをTrue

lineEditName
name入力
地物属性nameを表示
readOnlyをTrue


マップキャンバスのイベントを取得する


他のプラグインがどのように実装しているか調査する

マップキャンバスのイベントを取得する方法は手探りなので他のプラグインを覗いて調べてみました。

参考にしたのはこの2つです。

GeoCoding [GitHub]

Shape Tools [GitHub]


GeoCodingでは以下のようにClickToolのインスタンスをマップキャンバスのマップツールにセットしています。


GeoCoding.py

        ct = ClickTool(self.iface,  self.reverse_action);

self.previous_map_tool = self.iface.mapCanvas().mapTool()
self.iface.mapCanvas().setMapTool(ct)

ClickToolはマップツールを継承したクラスでリリース(マウスアップ)イベントをオーバーライドしています。

コンストラクタでコールバック用のメソッドを受け取ってイベント時に実行する仕組みですね。


Utils.py

class ClickTool(QgsMapTool):

def __init__(self,iface, callback):
QgsMapTool.__init__(self,iface.mapCanvas())
self.iface = iface
self.callback = callback
self.canvas = iface.mapCanvas()
return None

def canvasReleaseEvent(self,e):
point = self.canvas.getCoordinateTransform().toMapPoint(e.pos().x(),e.pos().y())
self.callback(point)
return None



Shape Toolsは以下のようにgeodesicMeasureToolのインスタンスをマップキャンバスのマップツールにセットしています。


shapeTools.py

    def initGui(self):

...
# Initialize Geodesic Measure Tool
self.geodesicMeasureTool = GeodesicMeasureTool(self.iface, self.iface.mainWindow())
...
def measureTool(self):
self.measureAction.setChecked(True)
self.canvas.setMapTool(self.geodesicMeasureTool)

geodesicMeasureToolもマップツールを継承したクラスでプレス(クリック)イベントとムーブイベントをオーバーライドしています。

こちらは画面を持っていてクラスの中でイベント処理をしています。


geodesicMeasureTool.py

class GeodesicMeasureTool(QgsMapTool):

def __init__(self, iface, parent):
QgsMapTool.__init__(self, iface.mapCanvas())
self.iface = iface
self.canvas = iface.mapCanvas()
self.measureDialog = GeodesicMeasureDialog(iface, parent)
...
def canvasPressEvent(self, event):
'''Capture the coordinates when the user click on the mouse for measurements.'''
if not self.measureDialog.isVisible():
self.measureDialog.show()
return
if not self.measureDialog.ready():
return
pt = event.mapPoint()
button = event.button()
canvasCRS = self.canvas.mapSettings().destinationCrs()
if canvasCRS != epsg4326:
transform = QgsCoordinateTransform(canvasCRS, epsg4326, QgsProject.instance())
pt = transform.transform(pt.x(), pt.y())
self.measureDialog.addPoint(pt, button)
if button == 2:
self.measureDialog.stop()

def canvasMoveEvent(self, event):
'''Capture the coordinate as the user moves the mouse over
the canvas.'''

if self.measureDialog.motionReady():
try:
pt = event.mapPoint()
canvasCRS = self.canvas.mapSettings().destinationCrs()
if canvasCRS != epsg4326:
transform = QgsCoordinateTransform(canvasCRS, epsg4326, QgsProject.instance())
pt = transform.transform(pt.x(), pt.y())
self.measureDialog.inMotion(pt)
except:
return



というわけでマップキャンバスのイベントを取得するには


  • QgsMapToolを継承したクラスを用意してイベントをオーバーライドする。


  • self.iface.mapCanvas().setMapTool()に処理したいマップツール継承クラスをセットする。

で良いようです。意外と簡単でした。


テストしてみましょう。


test_plugin.py

from qgis.gui import QgsMapTool

...
def dlgUpdate(self):
if self.first_flg:
...
self.dlg.btnCanvasEvent.toggled.connect(self.btnCanvasEventToggled)
...
def btnCanvasEventToggled(self, checked):
if checked:
self.dlg.btnCanvasEvent.setText('ON')
# 元のマップツールを退避
self.previous_map_tool = self.iface.mapCanvas().mapTool()
cmt = CustomMapTool(self.iface, self.CanvasPress)
self.iface.mapCanvas().setMapTool(cmt)

else:
self.dlg.btnCanvasEvent.setText('OFF')
# 元のマップツールに戻す
self.iface.mapCanvas().setMapTool(self.previous_map_tool)

def CanvasPress(self, event):
QMessageBox.information(self.dlg, 'info', 'press')

class CustomMapTool(QgsMapTool):
def __init__(self, iface, callback):
QgsMapTool.__init__(self, iface.mapCanvas())
self.callback = callback

def canvasPressEvent(self, event):
# コールバック関数を実行
self.callback(event)


プラグインをリロードしてから実行してキャンバスイベント切替ボタンをクリックしてマップツールを切り替えます。

マップキャンバスをクリックするとメッセージが表示されました。

もう一度キャンバスイベント切替ボタンをクリックしてマップツールを元に戻してからもう一度マップキャンバスをクリックするとメッセージは出なくなりました。

大丈夫そうです。


クリックイベントから座標を取得する方法

Shape Toolsの上記引用箇所にすでに答えが載っていますね。

こんな感じで取れました。

        pt = event.mapPoint()

lon = pt.x()
lat = pt.y()


座標から対象レイヤの地物を検索する方法

クリックした地点をピンポイントで検索するAPIは無いので、クリックした地点に矩形を作ってその矩形に含まれる地物を検索することになります。

QGIS2.18にあったmaplayer.select(rect)は無くなったのでmaplayer.selectByRect(rect,SelectBehaviour)を使います。

では矩形を作って検索してみましょう。


test_plugin.py

    def CanvasPress(self, event):

# アクティブレイヤを取得
act_layer = self.iface.activeLayer()
if act_layer is None or act_layer.type() != QgsMapLayer.VectorLayer:
return

pt = event.mapPoint()
# 検索バッファ
buffer = 0.001
rect = QgsRectangle(pt.x() - buffer,
pt.y() - buffer,
pt.x() + buffer,
pt.y() + buffer)
# SelectBehaviourはデフォルト(省略可能)
act_layer.selectByRect(rect)


プラグインをリロードして実行するとクリックした付近のポイントが選択されました。


一番近い点だけを選択状態にする方法

サンプルで使用しているレイヤは地物がはまばらなので問題ありませんが、実際のデータでは隣り合うデータがある場合に選択しようと思った地物以外が選択されてしまうこともあるでしょう。

そこでクリックした地点に一番近い地物だけを選択状態にする処理を入れてみます。

まずはテスト動作時にクリックしたら複数選ばれるように検索バッファを広げてみます。

buffer = 0.01

複数地物が選択されたらクリック地点との距離を計算して最も近い地物だけ選択状にします。


test_plugin.py

import math

...
feat_list = act_layer.selectedFeatures()
# 初期値を設定
min = None
fid = -1
for feat in feat_list:
geom = feat.geometry()
pointxy = geom.asPoint()
# 2点間の距離を計算
d = math.sqrt((pointxy.x() - pt.x()) ** 2 + (pointxy.y() - pt.y()) ** 2)
# 最短距離なら更新
if min is None or d < min:
min = d
fid = feat.id()
id = feat['id']
name = feat['name']

# 選択をクリア
act_layer.removeSelection()

if fid > -1:
act_layer.select(fid)
self.dlg.lineEditId.setText(str(id))
self.dlg.lineEditName.setText(name)


プラグインをリロードして実行するとクリックした付近のポイントが選択されて属性が表示されました。

今回はかなり適当な場所をクリックしても最も近いポイントが選択されるようになったと思います。


動作確認

この記事のプラグインをGitHubに公開しました。

https://github.com/ozo360/TestPlugin/tree/part5

プラグインからアクティブレイヤの地物選択ができるようになりました。

目的達成です。


まとめ

マップキャンバスのイベントを取得するには

QgsMapToolを継承したクラスを用意してイベントをオーバーライドする。

self.iface.mapCanvas().setMapTool()に処理したいマップツール継承クラスをセットする。

マップツールは使い終わったら元に戻す。

クリックイベントからポイントを取得するにはevent.mapPoint()を使う。

レイヤの検索はmaplayer.select(rect)からmaplayer.selectByRect(rect,SelectBehaviour)に変更。

2点間の距離を計算方法

math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)


最後に

QGISの基本的操作をプラグインから操作できるようになったと思います。

困った時は今回の記事でやったように既存プラグインのソースを読むとヒントが見るかることが多いのでお勧めですし、必要ならがっつりコピーしても(ライセンス的にも)大丈夫です。

プラグイン操作に限定するため座標系について極力省いて説明をしています。実際には意識して作らないといけないことが多いかもしれないですけど、とりあえず気軽に作ってみたら何とかなるはず。

QGISプラグイン制作についての記事で、なにかリクエストがあればコメントいただけたら嬉しいです。

以上です。


関連記事

QGIS3でpythonプラグインを作ってみた その1 ベース作成

QGIS3でpythonプラグインを作ってみた その2 QButtonと選択レイヤ取得について

QGIS3でpythonプラグインを作ってみた その3 QComboBoxとレイヤ取得について

QGIS3でpythonプラグインを作ってみた その4 地物の追加編集削除について

QGIS3でpythonプラグインを作ってみた その5 地図をクリックして地物を選択する


本記事のライセンス

クリエイティブ・コモンズ・ライセンス

この記事は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。