WMS の地図画像を Google Map 上に表示する

この記事は ケーシーエスキャロット Advent Calendar の記事です。

WMS とは、地図画像配信方式の一つです。 表示したい地図範囲を指定すると、その範囲を切り取った画像が返ってくるという方式でデータの配信を行います。

地理院地図|地理院タイルについて

これを Google Map 上に表示してみよう、というのが今回の内容です。

WMSAPI 仕様

今回利用しようと思っているWMSAPIは、大体以下のような仕様とします。

  • 範囲指定: 左下(南西)と右上(北東)の緯度経度を指定(世界測地系
  • サイズ指定: 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());

つまり WMSGoogle Map 上に表示するには?

getTileUrl で該当タイルに当てはまるWMSAPI を返すようにしてあげれば、表示することができそうです。

が、ここで問題があります。

getTileUrl に引数で渡されるのはタイル座標(X, Y)と zoom レベルなので、 緯度経度の情報を取得するには、タイル座標を緯度経度に変換する必要があるのです。

タイル座標から緯度経度への変換

タイル座標を緯度経度に変換する方法は、以下のサイトで紹介されていました。

TrailNote : 座標の変換(世界座標、ピクセル座標、タイル座標、緯度・経度)

つまり、

  1. タイル座標をピクセル座標に変換
  2. ピクセル座標と zoom レベルを使って、緯度経度に変換

というステップでタイル座標→緯度経度の変換ができそうです。

これをこんなかんじでコードにおこしてみます。

// タイル座標を緯度経度に変換する
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 を足しているのは、 単純にタイル座標→ピクセル座標変換すると左上のピクセル座標になってしまうためです。 f:id:aoiro6:20201218001739p:plain

下の図のように、左上を (0, 0) とすると、タイルの1辺は 256 px なので、左下と右上はそれぞれ (0, 255) と (255, 0) になるので、それぞれ255を足しています。 f:id:aoiro6:20201218001750p:plain

これでタイル座標から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;
  }
}

感想

これでWMSGoogle Map 上に表示させるという目的はクリアできましたが、 実際表示させてみるとめちゃくちゃ表示に時間がかかります。(正直実用的ではなさそう)

とはいえ、久々に数式見たり、頭を悩ませてコードを書いたりして良い経験になりました。

御覧いただきありがとうございました。