QGISプラグインの開発(2)

この記事は1年以上前に書かれました。
内容が古くなっている可能性がありますのでご注意下さい。

QGISプラグイン開発の基本

今回は、QGISプラグインの開発(1)で紹介した「PediaLayer」プラグインの開発方法について解説します。

開発環境の準備
開発するプラグインによって必要なツールは変わりますが、今回は以下のツールを使用します。
・QGIS本体
・(QGISにバンドルされた)Pythonの開発環境
・「Plugin Builder」プラグインと「Plugin Reloader」プラグイン
・Qtの開発環境(Qt Creator)
・makeコマンド(Windowsの場合はGnuWin32のmakeコマンド)

QGIS本体はインストールされているものとして、まずはPlugin BuilderプラグインとPlugin Reloaderプラグインをインストールします。いずれも公式プラグインですので、リポジトリの追加は必要ありません。

Plugin Reloaderプラグインは必須ではありませんが、プラグインのファイルを更新したときに、QGISを再起動することなく更新を反映させることができるので便利です。

Qtの開発環境は
http://www.qt.io/download-open-source/
からダウンロードします。セットアップの途中でQtアカウントが必要になりますので、まだアカウントを持っていない場合は新規登録します。
Windows環境でmakeコマンドが入っていない場合は、
http://gnuwin32.sourceforge.net/packages/make.htm
からMake for Windowsをダウンロードしてインストールします。

プラグインのテンプレート作成
はじめに、Plugin Builderプラグインを使用してプラグインのテンプレートを作成します。
QGISの「プラグイン」メニューから「Plugin Builder」→「Plugin Builder」を選択してPlugin Builderを起動し、ウィザードに従って情報を入力します。
最初の画面ではプラグインのクラス名、プラグイン名などの基本情報を入力します。

次にプラグインの説明を入力します。

次の画面では、プラグインの動作に関わる重要な選択をします。

Templateは、
・Tool button with dialog
・Tool button with dock widget
・Processing Provider
から選択します。今回はダイアログボックスを表示するのでTool button with dialogを選択します。
Menuは
・Plugins
・Database
・Raster
・Vector
・Web
から選択します。今回はWebを選択し、「Web」メニュー配下にプラグイン起動のメニューを設置します。メニュー項目名は「Create a layer from DBpedia」とします。

生成する項目の選択画面では、とりあえずすべてチェックを付けたままとします。

次の画面で、Bug trackerとRepositoryの入力は必須になっています。今回はGitHubで公開するため、GitHubで作成予定のリポジトリ名を入力します。

[Next]ボタンをクリックすると、プラグインのテンプレートを生成するフォルダの選択画面が表示されます。QGISのプラグインのインストール先(Windowsの場合)である
C:\Users\<ユーザ名>\.qgis2\python\plugins\
を指定します。こうすると、プログラムを更新した後、Plugin Reloaderプラグインで再ロードするだけですぐに更新を反映されることができます。
以下の画面が表示されればテンプレートの生成は完了です。



ダイアログボックスの編集

Plugin Builderプラグインが生成したUI定義ファイル「pedia_layer_dialog_base.ui」をQt Creatorで開きます。

最初は[OK]ボタンと[キャンセル]ボタンしかありませんので、DBpediaから情報を取得する範囲を指定するためのレイヤを選択するコンボボックスと、取得する情報の最大件数を指定するためのスピンボックス、それぞれのラベルを追加します。

プログラムの作成
Plugin Builderプラグインが生成した「pedia_layer.py」ファイルを編集します。PediaLayerクラスの定義の前で、以下のモジュールのインポートを追加します。

from qgis.core import *
import json, urllib, urllib2
from PyQt4.QtCore import *

runメソッドは2箇所、処理の追加をします。
まず、ダイアログボックスを表示する前に、コンボボックスの初期化処理を追加します。

# PREPARE COMBO BOX
self.dlg.comboBox.clear()
layers = self.iface.legendInterface().layers()
layer_list = []
for layer in layers :
    if isinstance(layer, QgsVectorLayer) or isinstance(layer, QgsRasterLayer) :
         layer_list.append(layer.name())
self.dlg.comboBox.addItems(layer_list)

QGISのAPIでレイヤの一覧を取得し、ベクタレイヤとラスタレイヤのみ、コンボボックスに名前を追加します。
ダイアログボックスで[OK]ボタンがクリックされたときの、レイヤ追加処理は以下の通りです。

# ADD LAYER FROM DBPEDIA
# calculate coordinates
index = self.dlg.comboBox.currentIndex()
layer = layers[index]
extent = layer.extent()
srcCrs = layer.crs()
destCrs = QgsCoordinateReferenceSystem(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
transform = QgsCoordinateTransform(srcCrs, destCrs)
wgsExtent = transform.transform(extent)
xMax = wgsExtent.xMaximum()
xMin = wgsExtent.xMinimum()
yMax = wgsExtent.yMaximum()
yMin = wgsExtent.yMinimum()

# prepare query
sparql = "SELECT distinct ?name ?abstract ?lat ?lon ?url\n" \
+ "WHERE {\n" \
+ "?s rdfs:label ?name ;\n" \
+ "dbpedia-owl:abstract ?abstract ;\n" \
+ "foaf:isPrimaryTopicOf ?url ;\n" \
+ "geo:lat ?lat ;\n" \
+ "geo:long ?lon .\n" \
+ "FILTER ( " \
+ "?lon > \"" + str(xMin) + "\"^^xsd:float && ?lon < \"" + str(xMax) + "\"^^xsd:float && " \ + "?lat > \"" + str(yMin) + "\"^^xsd:float && ?lat < \"" + str(yMax) + "\"^^xsd:float)\n" \
+ "FILTER (LANG(?name)='ja' && LANG(?abstract)='ja')" \
+ "\n} LIMIT " + str(self.dlg.spinBox.value())
server = "http://ja.dbpedia.org/sparql"
param = { "query" : sparql , "format" : "application/sparql-results+json" }

# exec query
request = urllib2.Request(server, urllib.urlencode(param))
response = urllib2.urlopen(request)
data = json.loads(response.read())
list = data["results"]["bindings"]

# add layer
newLayer = QgsVectorLayer("Point?crs=epsg:4326", "pedialayer", "memory")
newLayer.setProviderEncoding("UTF-8")
QgsMapLayerRegistry.instance().addMapLayer(newLayer)
newLayer.startEditing()
newLayer.addAttribute(QgsField("name", QVariant.String))
newLayer.addAttribute(QgsField("url", QVariant.String))
newLayer.addAttribute(QgsField("abstract", QVariant.String))

# add features
for item in list:
    feature = QgsFeature(newLayer.pendingFields())
    feature.setGeometry(QgsGeometry.fromPoint(QgsPoint(float(item["lon"]["value"]), float(item["lat"]["value"]))))
    feature.setAttribute("name", unicode(item["name"]["value"]))
    feature.setAttribute("url", unicode(item["url"]["value"]))
    feature.setAttribute("abstract", unicode(item["abstract"]["value"]))
    newLayer.addFeature(feature)
newLayer.commitChanges()
newLayer.updateExtents()

まずはじめに、コンボボックスで選択されたレイヤから、緯度経度の最大値と最大値を取得します。

# calculate coordinates
index = self.dlg.comboBox.currentIndex()
layer = layers[index]
extent = layer.extent()
srcCrs = layer.crs()
destCrs = QgsCoordinateReferenceSystem(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
transform = QgsCoordinateTransform(srcCrs, destCrs)
wgsExtent = transform.transform(extent)
xMax = wgsExtent.xMaximum()
xMin = wgsExtent.xMinimum()
yMax = wgsExtent.yMaximum()
yMin = wgsExtent.yMinimum()

レイヤによって座標参照システム(CRS)が異なる場合があるので、WGS84(EPSG:4326)に変換しています。
次に、DBpedia Japaneseに対して発行するSPARQLクエリを作成します。

# prepare query
sparql = "SELECT distinct ?name ?abstract ?lat ?lon ?url\n" \
+ "WHERE {\n" \
+ "?s rdfs:label ?name ;\n" \
+ "dbpedia-owl:abstract ?abstract ;\n" \
+ "foaf:isPrimaryTopicOf ?url ;\n" \
+ "geo:lat ?lat ;\n" \
+ "geo:long ?lon .\n" \
+ "FILTER ( " \
+ "?lon > \"" + str(xMin) + "\"^^xsd:float && ?lon < \"" + str(xMax) + "\"^^xsd:float && " \ + "?lat > \"" + str(yMin) + "\"^^xsd:float && ?lat < \"" + str(yMax) + "\"^^xsd:float)\n" \
+ "FILTER (LANG(?name)='ja' && LANG(?abstract)='ja')" \
+ "\n} LIMIT " + str(self.dlg.spinBox.value())
server = "http://ja.dbpedia.org/sparql"
param = { "query" : sparql , "format" : "application/sparql-results+json" }

クエリを発行し、結果はJSON形式で取得します。

# exec query
request = urllib2.Request(server, urllib.urlencode(param))
response = urllib2.urlopen(request)
data = json.loads(response.read())
list = data["results"]["bindings"]

結果が得られたら、新しいレイヤを作成して、地物を追加します。
pedialayerというレイヤをメモリ上に作成し、編集状態にして「name」「url」「abstract」属性を追加します。

# add layer
newLayer = QgsVectorLayer("Point?crs=epsg:4326", "pedialayer", "memory")
newLayer.setProviderEncoding("UTF-8")
QgsMapLayerRegistry.instance().addMapLayer(newLayer)
newLayer.startEditing()
newLayer.addAttribute(QgsField("name", QVariant.String))
newLayer.addAttribute(QgsField("url", QVariant.String))
newLayer.addAttribute(QgsField("abstract", QVariant.String))

最後に、クエリ結果から1件ずつ緯度経度、項目名、URL、概要説明文を取り出して、地物を追加して属性をセットします。

# add features
for item in list:
    feature = QgsFeature(newLayer.pendingFields())
    feature.setGeometry(QgsGeometry.fromPoint(QgsPoint(float(item["lon"]["value"]), float(item["lat"]["value"]))))
    feature.setAttribute("name", unicode(item["name"]["value"]))
    feature.setAttribute("url", unicode(item["url"]["value"]))
    feature.setAttribute("abstract", unicode(item["abstract"]["value"]))
    newLayer.addFeature(feature)
newLayer.commitChanges()
newLayer.updateExtents()

makeコマンドでコンパイルすればプラグインの完成です。

完全なソースコード一式はGitHubで公開しています。

QGISプラグインの開発 12345