QGIS3でpythonプラグインを作る その5
目標
前回作ったプラグインに、地図(マップキャンバス)をクリックして地物の選択ができる機能を追加する。
押さえたいところは
- マップキャンバスのイベントを取得する方法
- クリックイベントから座標を取得する方法
- 座標から対象レイヤの地物を検索する方法
- 一番近い点だけを選択状態にする方法
下準備
- ポイントのシェープファイルがレイヤ登録されたQGISプロジェクトを用意。ダウンロードはこちら
- 前回作ったプラグインを有効にしておく。GitHubのリポジトリはこちら
- QGISのPluginReloaderプラグインを有効にしておく(任意)
GUI編集
QGIS付属の「Qt Designer with QGIS 3.0.0 custom widgets」を使ってGUIを編集します。
目標だけならボタン1個でいいのですが属性を表示するエディットラインも付けておきます。
物理名 | 論理名 | 備考 |
---|---|---|
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のインスタンスをマップキャンバスのマップツールにセットしています。
ct = ClickTool(self.iface, self.reverse_action);
self.previous_map_tool = self.iface.mapCanvas().mapTool()
self.iface.mapCanvas().setMapTool(ct)
ClickToolはマップツールを継承したクラスでリリース(マウスアップ)イベントをオーバーライドしています。
コンストラクタでコールバック用のメソッドを受け取ってイベント時に実行する仕組みですね。
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のインスタンスをマップキャンバスのマップツールにセットしています。
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もマップツールを継承したクラスでプレス(クリック)イベントとムーブイベントをオーバーライドしています。
こちらは画面を持っていてクラスの中でイベント処理をしています。
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()
に処理したいマップツール継承クラスをセットする。
で良いようです。意外と簡単でした。
テストしてみましょう。
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)
を使います。
では矩形を作って検索してみましょう。
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
複数地物が選択されたらクリック地点との距離を計算して最も近い地物だけ選択状にします。
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 国際 ライセンスの下に提供されています。