Quantum GIS / OpenLayers Plug-in
はじめに
javascript 学習を始めて最初に出くわす中ボスとして、QGIS の OpenLayers プラグインを解読し電子国土版を作成してみようというのである。といっても、プラグインは python で書かれており、私は python は分からない。なので、分かる範囲での解読に限定される。
また、javascript がなんとか理解できるようになったのは今年になってからである。以下の記事にはツッコミどころも当然あるだろうと考える。遠慮なくツッコミを入れて頂けると有り難い。
python の部分
プラグインのフォルダを開くと、いくつか python のファイルがあるほか、3つのフォルダ(html, ui, i18n)と、いくつかのアイコンが格納されている。このうち、i18n は各国語対応のフォルダ(のはず)で、ui は jQuery のユーザインターフェースの筈である。
エディタで順に見ていくと、openlayers_plugin.py が一番キモになるファイルに見える。といっても、私には殆ど分からない。ただ、124行目に #Layers とあり、直後の行から self.olLayerTypeRegistry() という関数が幾つも並んでいる。引数はいずれも OlLayerType() という関数で、この関数は4つもしくは5つの引数を持っている。
第一引数:全て’self’ である。
第二引数:’Google Physical’, ‘OpenStreetMap’, ‘Bing Aerial’ 等と書かれているので、表示しようとする地図レイヤの種別であろうと推定される
第三引数:対応するアイコンであろうと推定される
第四引数:地図を表示させるための html ファイルであろうと推定される
第五引数:一つを除いて全て True が指定されている(唯一の例外は、第五引数が存在していない)
なので、もしも電子国土を地図として表示させたいのであれば、第二引数に ‘CyberJapan’ など電子国土らしい名前を指定し、第三引数に電子国土のアイコンを指定し、第四引数に専用の地図描画 html を用意すれば良いように思われる。
ここまでは、わざわざ書かなくても、誰でも分かる話である。問題は、html フォルダにどのような内容のファイルを追加するか、である。そこで、既存の html ファイルを眺めて、共通パターンがないか探ってみた。
html の解読:apple.html を例にとって
apple.html を覗くと、こういう構成になっている:
スタイルシートや javascript をインクルードする宣言部分
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>OpenLayers Apple iPhoto map Layer</title> <link rel="stylesheet" href="qgis.css" type="text/css"> <script src="OpenLayers.js"></script> <script src="OlOverviewMarker.js"></script>
javasecipt のセクション(ここがメイン)
といっても、グローバル変数を3つと、このプラグインを起動した時に一回だけ呼び出される init 関数を定義しているだけである。
<script type="text/javascript">
グローバル変数
var map;
(OpenLayers.Map オブジェクトのmap インスタンス)
var loadEnd;
(Boolean 型の変数)
var oloMarker; // OpenLayer Overview Marker
(QGIS の全体図マーカーと関係があるらしい)
init 関数
function init() { map = new OpenLayers.Map('map', {
・OpenLayers.Map コンストラクタを呼び出してインスタンス map を生成している。
・このコンストラクタを呼び出す際には、いくつか引数を指定するのが通例である。ここでは、以下のプロパティを指定している:
theme: null,
(意味不明、なくても困らないと考える)
controls: [],
(このmapオブジェクトのコントロールは何も用意しない→全て Quantum GIS 側で処理するから)
projection: new OpenLayers.Projection("EPSG:3857"),
(デフォルトで使用する CRS として 3857 -真球疑似メルカトル投影- を使用する)、
units: "m",
(使用する単位はメートルである)
maxResolution: 156543.0339,
(最小縮尺における 1pixel のサイズで、単位はメートル。赤道周囲長約4万kmの 1/256 で与えられる)
maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34)
(描画範囲、東経0度北緯0度を座標原点とみて、東西南北に約2万kmの範囲を指定している。この値は、真球疑似メルカトル図法を利用していることと関係がある。当然、極は表示できない。)
}); loadEnd = false;
(初期状態では、地図の読み出しが終わっていない、ということ)。
function layerLoadStart(event) { loadEnd = false; } function layerLoadEnd(event) { loadEnd = true; }
・これら2つの関数は、地図レイヤの読み出し完了に関するイベントと連動して変数 loadEnd を変化させる。
・続いて、変数 apple に地図レイヤを生成している。具体的には、タイル型の地図として、OpenLayers.Layer.XYZ を呼び出している(これは、電子国土でも同じ)。
var apple = new OpenLayers.Layer.XYZ( "Apple iPhoto map", "http://gsp2.apple.com/tile?api=1&style=slideshow&layers=default&lang=en&z=${z}&x=${x}&y=${y}&v=9", { sphericalMercator: true, wrapDateLine: true, // TODO: min zoom level 2 numZoomLevels: 15, // attribution: "", // FIXME: attribution eventListeners: { "loadstart": layerLoadStart, "loadend": layerLoadEnd
・ここで、コンストラクタの呼び出しに際して、イベントリスナにloadstart, loadend の2イベントを登録している(処理関数は先ほど定義したlayerLoadStart, layerLoadEnd の2関数である)。地図レイヤの生成にイベントリスナを定義しているので、データの読み出しをウォッチするのだなと推定できる。
} } ); map.addLayer(apple);
・生成した地図レイヤを地図( map インスタンス)に登録している。
map.addControl(new OpenLayers.Control.Attribution());
・著作権表示処理を実施している。ただし、attribution には空文字列がセットされており、「誰か直して」とコメントされている(笑)。
map.setCenter(new OpenLayers.LonLat(0, 0), 3);
・初期表示として、東経0度、北緯0度、ズームレベル3を指定している。
oloMarker = new OlOverviewMarker(map, getPathUpper(document.URL) + '/x.png');
・最後に、OlOvervierMarker を呼び出している( x.png は、赤いXの印のアイコンであるが、プラグインでこれが表示される様子はなく、意味不明である)。
}
</script>
・スクリプトはこれで終わりである。
</head>
body 部分
body は非常にシンプルで、
<div id=”map”></div>
があるだけである。
スクリプトで定義した地図インスタンス map が結びつけられ、地図として表示される。
<body onload="init()"> <div id="map"></div> </body> </html>
ちなみに、この html は単体でも動作する。その場合は、配列 control が空なので、表示された地図を操作することはできず、デフォルトの表示位置及び表示ズームレベルで表示される。
・また、たとえば下記のような簡単な html からこの apple.html を呼び出した場合でも動作し、iframe の内部一杯にちゃんと地図が表示される。
( OloverviewMarker は無視される)
<html> <head></head> <body> <iframe src=”apple.html” width=”500” height=”500”> </iframe> </body> </html>
(文法的に完全な html ではないが、シンプルさを強調するためにあえてこういうコーディングにしている)
・このサンプルプログラムは、Quantum GIS における OpenLayers plug-in の挙動を知る上で本質的であると考える。Quantum GIS は、実質的には、こういう iframe を plug-in 側の html に提供すると共に、適宜 OpenLayers が受け取れるようなコントロールイベントを渡しているのである。
(サンプルプログラムにはそういうイベントを渡す処理は書かれていないが、例えば【ズームレベルを一段上げる】というボタンを設置し対応するイベントを plug-in 側の html に渡すような処理ならば、javascript で実装可能と考える。)
電子国土の場合
それでは、電子国土ではどうなるか。
実は、意外に難しくないと考えられる。
考え方
・map インスタンスを生成する部分などは同じで、
・地図レイヤとして webtisMap を生成する。これは webtis_v4.js を使えば簡単に実現できる( OpenLayers.Layer.XYZ をプロトタイプに持つクラスである)。
・イベントリスナのコールバックを忘れずに挿入する。
これは、地図レイヤの生成時に引数として渡しても良いが、引数生成後に
map.events.register('loadstart', map, layerLoadStart); map.events.register('loadend', map, layerLoadEnd);
を定義する方法も考えられる。両方試してみたが、この方法の方が優れているようだ(通常の方法では、実行時に原因不明のタイル抜けが頻発する)。
・国土地理院のサーバでは、ズームレベル 15 - 17 において空中写真オルソ画像を表示させることも可能である。こちらの場合は、layer インスタンスを作成した直後に
var mapMeta = {dataId: "DJBMO" }; // オルソ画像のタイル情報 webtisOrtho.setMapMeta(15,mapMeta); webtisOrtho.setMapMeta(16,mapMeta); webtisOrtho.setMapMeta(17,mapMeta);
とすることで、ズームレベル 15 - 17 を空中写真オルソ画像に変更できる。
・実は、webtis には getOrthoDataSet というメソッドが用意されており、最初からオルソ画像をタイル情報に含むようなデータセット(ズームレベルとタイル情報のペアからなるハッシュ情報)を返してくれる。なので、地図レイヤ生成時に
cjpOrtho = new webtis.Layer.BaseMap(‘webtisOrtho’,new getOrthoDataSet()) ;
のようなコーディングが効けばエレガントなのであるが、残念ながら getOrthoDataSet はコンストラクタではないので、この記法は使えない。
実装、既知のバグについて
・OpenLayers plugin にはバグが存在する。プラグインのバグか Quantum GIS のバグかについても切り分けつつ、既知のバグを報告する。
Quantum GIS 立ち上げ直後に OpenLayers Plugin を起動するとエラーになる
【現象】
・このエラーは、QGIS 立ち上げ直後の画面サイズに拠ります。1200×800 位のディスプレイだと 100% 再現しますが、1920×1280 くらいの広いディスプレイだと再現しない場合もあります。
・プラグインを呼び出す前に、オプション画面でシステムの CRS を EPSG:3857 に変更すると、今度はエラーを 100% 防ぐことができます。
・電子国土に限定されず、Google でも OSM でもどれでも起こります。ただし、もとになる OpenLayers.Layer のタイプが異なる場合に全て該当するかどうかは未検証です。
【原因推定】
・エラーメッセージを見る限り、OpenLayers.LonLat(x,y).transform() 周辺が出しているように見えます( transform が直接出しているわけではなさそうですが)。
・デフォルトでは CRS として EPSG:4326 が定義されています。
これでOpenLayers.Bounds(-2000万, -2000万, 2000万, 2000万) や
maxResolution = 156543.0339 のようなコードを実行した場合に、これが度だと考えるとおかしなことになります。縮尺計算をした結果、NaN が帰ってくるということだろうと考えます。
・画面が広いと、辛うじて縮尺が NaN にならずに済むのではないか、とも推定されます。
・事前に CRS を EPSG:3857 に変更してあれば、上記コードを正しく解釈できるので、エラーにならないのでしょう。
【解析】
・これは間違いなく Quantum GIS 本体のバグです。
その証拠に、
<html> <head></head> <body> <iframe src=”apple.html” width=”500” height=”500”> </iframe> </body> </html>
のような簡素な html から(QGISからではなく)plugin 同梱の html を呼び出した場合は絶対にエラーになりません。iframe には CRS の概念がないので、CRS マッチング処理をする必要がないからです。
・Quantum GIS では、CRS マッチングをする必要があるので、現在の表示画面における四隅座標( CRS に基づく値)を plug-in に与えてどのような範囲でタイルをだすのか計算させているはずです。この計算で NaN が発生していると見られます。
・Quantum GIS の立ち上げ直後に CRS を EPSG:3857 に変更すれば、エラーは 100% 防げますが、EPSG:4326 は利用頻度が高いと思われますので、これをユーザに要求するのは無理でしょう。
・ところで、地図レイヤによっては、日付変更線を越えてサイクリックに横長の画像を表示するものがあります。そういえば、この場合の挙動を調べていませんでした。
【対策案】
・このプラグインを呼び出す場合に、必ず先に何か shape ファイルその他の別レイヤを読み出しておくことにすれば、エラーは防げます。
・現実的な縮尺で地図が表示されていれば、OpenLayers Plugin はその縮尺を踏襲しつつ CRS を強制的に EPSG:3857 に変更しますので、計算の途中で NaN になるようなことがなくなります。
QGIS ver.1.6.0 の場合は、1回目に必ずエラーになる
【現象】
・Quantum GIS のバージョンが 1.6.0 の場合は、初めてこのプラグインを起動した場合にかなりの確率でエラーになります。ただし、もう一度プラグインを起動すると、何事もなかったかのように動作してくれます。
・電子国土、Google、OSM など、表示レイヤの種類に関係なく発生します。
・バージョン 1.7.x では発生しません。バージョン 1.8.x は試していません。
【原因、解析】
・よく分かりません。
・初期化処理が途中でエラーになるものの、QGIS 内の環境を一部改良してくれるので、もう一度走らせると今度はうまくいく、ということのように思えます。
【対策案】
・あまり気にせず使い続けて大丈夫だと考えます。ver.1.7.x 以降を使えばそもそも発現しませんし。
電子国土で、1/1500万程度の縮尺で表示させると、表示が乱れることがある
【現象】
・電子国土で、ズームレベル0から徐々に拡大していくと、ズームレベル5または6位で地図画像が東西にずれて表示されることがあります。
・Quantum GIS でパンしても、画像は変わってくれません(依然、画像が偏った位置に表示される)。
・この状態でさらに拡大すると、場合によっては白抜けの画面になる場合があります。
【原因、解析】
・電子国土レイヤでは、ズームレベル5-8の地図タイルとして「1/500万日本とその周辺」から起こしたデータを使用しています。この「1/500万日本とその周辺」は、もともと日本付近のみで作成された紙地図で、図郭は縦長でした。
・ズームレベル5〜6程度では、画面の左右にタイルが存在しないような表示を求めることが起こりえます。表示範囲が一旦ここに嵌ると、表示が崩れるようです。
・ズームレベル5〜6で、NoData タイルの扱いが不正になっているものと考えられます。電子国土サーバ側のエラーの可能性もあります。
【対策案】
・先に何かデータを表示させておけば(1/1000万程度より表示縮尺が大きければ)、このエラーは発生しません。
OpenLayers plugin を表示させた後で、CRS を EPSG:3857 以外に変更すると、表示が崩れる
【現象】
・OpenLayers plugin を表示させると、CRS は自動的に EPSG:3857 に変更されます。
・ここで、Quantum GIS のオプション画面で CRS を強制的に他の値に変更すると、on-the-fly の有無に関係なく、表示が崩れます。具体的には、OpenLayers で表示される地図タイルと、それ以外のレイヤがぴったり重ならなくなり、OpenLayers 地図タイルの表示範囲が狭くなります。
【原因、解析】
・OpenLyaers の地図タイルは、EPSG:3857 以外では表示できません。map インスタンスが project:{EPSG:3857} で生成されている上に、OpenLayers にはmap.changeCRS のようなメソッドが用意されていません。
・map インスタンスを生成する際に、displayProjection を指定すればと思って試して見ましたが、EPSG:4326 でもダメでした。効いていない感じです。
なので、そもそも OpenLayers の内部変数 displayProjection は命名もしくは仕様が不正だと思います。
・また、map インスタンス生成時に displayProjection に EPSG:4326 を指定しても、Quantum GIS 側も認識しません。
本来、(1)データの座標記述に使用されている CRS、(2)画面で表示する際の CRS、(3)ユーザが座標指定する際の CRS の3つを上手く使い分ける必要があります。(3)は、QGISでは地図表示メインウインドウの下に表示されるマウス位置のことを想定しています。(2)のCRSとして EPSG:3857 や EPSG:3100( JGD2000 / UTM54N )などを使ったとしても、座標値は LatLon で欲しい場合があると思うのですが、どうでしょうか。
・まあ、それ以前に、(1)のケースと(2)のケースのどちらの CRS を指定するべきかが明瞭でない場面が多いですね。GIS の初心者を悩ませる高いハードルの一つだと思います。
・将来の QGIS では、on-the-fly が充実して、CRS を自由に変更できるようになるかも知れません。
・一方、OpenLayers 側が proj4 を内包して対応する、という可能性もあると思います。
【対策案】
・一旦 OpenLayers プラグインを表示させた場合は、CRS は EPSG:3857 で我慢するしかありません。
実装方法
電子国土の地図を表示するための html ファイル( cjp_mao.html )は https://dl.dropbox.com/u/47691954/cjp_map.html に、
また、電子国土のオルソ画像を表示するための html ファイル( cjp_ortho.html )は https://dl.dropbox.com/u/47691954/cjp_ortho.html に、それぞれ置いてあります。
電子国土のアイコンは国土地理院のサイトからでも取得できると思いますが、一応 https://dl.dropbox.com/u/47691954/cjp_icon.png に置いてあります。
実装方法は、大体下記の手順で良いと思います。Quantum GIS についての最小限度の知識を仮定して書いています。また、Windows ユーザを念頭において書いています。
- C:\Users\【username】\.qgis\python\plugins\openlayers フォルダを開く
- 【username】は、Windows のログインユーザ名です
- 当たり前ですが、OpenLayers Plugin は既にインストールされているものとします。
- openlayers_plugin.py をエディタで開く
- プラグインのバージョンにもよりますが、122行目付近に #Layers という行があるはずです
- その直後から self.openlayerTypeRegistry.add という関数が10行くらい書かれている筈です
- この並びの最後に、以下のように引数を変更した行を追加します。
- さらに、以下のように引数を変更した行を追加します。
- このフォルダに、cjp_icon.png をコピーします。
- このフォルダの下にある html フォルダに、cjp_map.html と chp_ortho.html をコピーします。
これで Ok です。Quantum GIS を立ち上げて、OpenLayers プラグインを呼び出すと、ちゃんと最下行に電子国土のアイコンと共に、Add CyberJapanMap Layer 及び Add CyberJapanOrtho Layer という行が登場し、これらを選ぶと電子国土の地図(またはオルソ画像)が表示されます。
なお、オルソ画像はズームレベル 15 から 17 だけ用意されていますので、表示縮尺 1/5,000〜1/40,000 くらいでないと表示されません。それ以外の縮尺の場合は、地図が表示されます。
おわりに
はてなダイアリーに書いてみましたが、もとのデザインをちょっと変わった設定にしてしまっていましたので、あまり見やすくなく、申し訳ありません。特に、super-pre 記法で html モードとか javascript モードを使ってみたところ、不必要にカラフルになってしまい、ちょっと自分でも引いています(汗)。
最初に書いたように、ツッコミ大歓迎です。よろしくお願い致します。