WMS の地図画像を Google Map 上に表示する
この記事は ケーシーエスキャロット Advent Calendar の記事です。
WMS とは、地図画像配信方式の一つです。 表示したい地図範囲を指定すると、その範囲を切り取った画像が返ってくるという方式でデータの配信を行います。
これを Google Map 上に表示してみよう、というのが今回の内容です。
WMS の API 仕様
今回利用しようと思っているWMSのAPIは、大体以下のような仕様とします。
- 範囲指定: 左下(南西)と右上(北東)の緯度経度を指定(世界測地系)
- サイズ指定: px単位
Google Map 上に画像を表示する方法
まず Google Map でどのように地図データの画像を表示するかです。
Google Map では 256 × 256 に画像を分割し表示させる、タイル方式という方法で地図画像の表示を行っています。 タイルがどうなっているかは、こちらのサイトを見るとイメージしやすいかと思います。
実際に Google Maps API でタイルを表示する場合、 overlayMapTypes
を用いて、以下のようなコードになります。
コードからなんとなくわかる通り Google Maps API では、
必要なタイル座標とzoomレベルを getTileUrl
に渡し、返ってきたURL(API)から画像を取得して表示するという動きをします。
// オーバーレイオブジェクト作成用 class CoordMapType { constructor(tileSize) { this.tileSize = tileSize; } MapOption() { let layer = new google.maps.ImageMapType({ tileSize: this.tileSize, isPng: true, getTileUrl: function(point, zoom) { // タイル取得のためのAPIを設定 url = "https://XXXXXXXXXX/{x}/{y}/{z}" .replace('{z}', zoom) .replace('{x}', point.x) .replace('{y}', point.y) return url; }, opacity: 0.7 }); return layer; } } // Google Map オブジェクト作成 var map = new google.maps.Map(document.getElementById("map"), { zoom: 10, center: { lat: 35.68, lng: 139.76 }, }); // オーバーレイオブジェクト作成、Google Map に追加 opts = new CoordMapType(new google.maps.Size(256, 256)); map.overlayMapTypes.insertAt(0, opts.MapOption());
つまり WMS を Google Map 上に表示するには?
getTileUrl
で該当タイルに当てはまるWMS の API を返すようにしてあげれば、表示することができそうです。
が、ここで問題があります。
getTileUrl
に引数で渡されるのはタイル座標(X, Y)と zoom レベルなので、
緯度経度の情報を取得するには、タイル座標を緯度経度に変換する必要があるのです。
タイル座標から緯度経度への変換
タイル座標を緯度経度に変換する方法は、以下のサイトで紹介されていました。
TrailNote : 座標の変換(世界座標、ピクセル座標、タイル座標、緯度・経度)
つまり、
というステップでタイル座標→緯度経度の変換ができそうです。
これをこんなかんじでコードにおこしてみます。
// タイル座標を緯度経度に変換する function tilePointToLatLon(x, y, zoom) { // 左下: タイル座標->ピクセル座標 let ldX = x * 256; let ldY = y * 256 + 255; // 左下: ピクセル座標->緯度経度 dl = pixPointToLatLon(ldX, ldY, zoom); // 右上: タイル座標-> ピクセル座標 let urX = x * 256 + 255; let urY = y * 256; // 右上: ピクセル座標->緯度経度 ur = pixPointToLatLon(urX, urY, zoom); return { down_left: dl, up_right: ur }; } // ピクセル座標を緯度経度に変換する function pixPointToLatLon(x, y, zoom) { // 表示可能上限 // L = 180 / PI * asin(tanh(PI)) = 85.05112878 const L = 85.05112878; // 経度 let lon = 180 * ((pixX / Math.pow(2, zoom + 7)) - 1); // 緯度 let lat = 180/Math.PI * (Math.asin(Math.tanh(-Math.PI/Math.pow(2, zoom+7) * pixY + Math.atanh(Math.sin(Math.PI/180 * L))))); return { lat: lat, lon: lon } }
タイル座標→ピクセル座標の箇所で、左下の y座標と右上のx座標に 255 を足しているのは、 単純にタイル座標→ピクセル座標変換すると左上のピクセル座標になってしまうためです。
下の図のように、左上を (0, 0) とすると、タイルの1辺は 256 px なので、左下と右上はそれぞれ (0, 255) と (255, 0) になるので、それぞれ255を足しています。
これでタイル座標からWMS指定に必要な緯度経度が取得できました。
後は、 getTileUrl
にこの変換ロジックを使ってWMSをタイルっぽく取得できるようにしてあげれば表示ができます。
落とし穴…
しかしこれ、実はIEだと動きません。
なぜかというと、緯度経度変換処理の中で使用している Math.tanh()
と Math.atanh()
がIEではサポートされていないからです。(なんてこった)
そのため、IEでも表示させたい場合はそれぞれ代替の計算式を使って、以下のように書き換えてあげると良いと思います。
// ピクセル座標を緯度経度に変換する function pixPointToLatLon(x, y, zoom) { // 表示可能上限 // L = 180 / PI * asin(tanh(PI)) = 85.05112878 const L = 85.05112878; // 経度 let lon = 180 * ((pixX / Math.pow(2, zoom + 7)) - 1); // 緯度 let lat = 180/Math.PI * (Math.asin(tanh(-Math.PI/Math.pow(2, zoom+7) * pixY + atanh(Math.sin(Math.PI/180 * L))))) return { lat: lat, lon: lon } } function tanh(x) { if (Math.tanh) { return Math.tanh(x); } else { var a = Math.exp(+x), b = Math.exp(-x); return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (a + b); } } function atanh(x) { if (Math.atanh) { return Math.atanh(x); } else { // |x| < 1 以外の場合は考慮していない if (Math.abs(x) < 1) { return Math.log((1+x)/(1-x)) / 2; } return Nan; } }
感想
これでWMSを Google Map 上に表示させるという目的はクリアできましたが、 実際表示させてみるとめちゃくちゃ表示に時間がかかります。(正直実用的ではなさそう)
とはいえ、久々に数式見たり、頭を悩ませてコードを書いたりして良い経験になりました。
御覧いただきありがとうございました。