真ん中のタイルを1枚選ぶ:グーグルマップのしくみを探る(5)

前回までは、座標変換について、若干細かく探りました。
グーグルマップといえば、スムーズな地図の動作が特徴ですが、
そのような動作の実現に不可欠なのが、緯度経度とピクセル座標の間の素早い座標変換なのです。

その座標変換は非常にシンプルに行われているのが、前回まででわかって頂けたと思います。
今回は、グーグルのAPIを使わずに、地図画像タイルを作成してみます。

グーグルマップのマップタイルは同じサイズで隙間なく敷き詰められているので、
一枚のタイルの位置が決めれば、他の位置も決まります。

今回はタイルを1枚だけ作成するプログラムをつくることにより、グーグルマップのタイルワークの第一歩を探ります。タイルは地図を表示するフレーム(ビューポート)の中心点を含む1枚とします。

地図画像

以下はグーグルマップを表示するのに最もシンプルなコードです。

function initialize() {
  var point = new google.maps.LatLng(35.68, 139.77);
  var myOptions = { 
           zoom: 15,  
           center: point,
           mapTypeId: google.maps.MapTypeId.ROADMAP 
  }; 
  var map = new google.maps.Map(document.getElementById("map"), 
                                                       myOptions); 
}  

このように、地図を表示するには、まず最初に、ズームレベル地図の中心(経緯度)地図の種類、が最低限必要です。

つまり、ズームレベル地図の種類から、対象とする地図画像を指定し、
地図の中心(経緯度)から、表示する位置を決めるのです。

ズームレベルが地図画像の大きさを表します。画像の辺長は、
ズームレベル0で256×20=256px、ズームレベル19で256×219=134,217,728pxとなります。

因みに、ズームレベルが0~19まであるとすると、地図の種類がROADMAP、SATELLITE、HYBRID、TERRAINの4種類としても、少なくとも80の地図画像がグーグルのサーバにあることになります。

タイル座標

グーグルマップでは地図画像を256px×256pxのタイル画像に分割してサーバに格納しています。
このタイル画像は、ピクセル座標と同じように左上を原点(0,0)として、(x,y)で座標が付けられています。

タイル座標は、地図画像上の位置であるピクセル座標を256で割ったときの商の整数部になります。

地図画像は、ズームレベルが大きくなれば、サイズが非常に大きくなってしまうので、
256pxで分割して、表示に必要な部分だけを呼び出して、貼り付けているいるのです。

1枚のタイル画像は、以下のようにURLでタイル画像とタイル座標を指定して取得しています。
http://mt3.google.com/vt?z=15&x=29105&y=12903
注意!
残念ながら、上のアドレスを直接呼び出すことはグーグルの規約で禁止されています。
あくまでも、仕組みを理解するためのものとして、参考に止めてください。

上の例は、ズームレベル15、タイル座標(29105,12903)の通常の交通地図のタイルです。
全世界の交通地図はmt0.google.com~mt3.google.comのドメインで管理されているようです。
(航空地図などは各国毎に管理されていると思われます。例えば、日本ならkhm0.google.co.jpなど)

緯度経度からタイル座標を求める

では、これまで探ってきたことを使って、ある地点の緯度経度が分かっているとき、
その地点を含むタイル座標を求めるコードを考えてみます。

以降の計算では、度単位ではなくラジアンを使うので変換した値を設定しておきます。

var point = new google.maps.LatLng(35.68, 139.77);
var lat_rad = point.lat() * Math.PI / 180;
var lng_rad = point.lng() * Math.PI / 180;

この緯度経度の値に対応する世界座標を求めます。
第3回目で導いた式を用います。

var worldCoord = new google.maps.Point(0,0);
var R = 128 / Math.PI;
worldCoord.x = R * (lng_rad + Math.PI);
worldCoord.y = - R / 2 *
    Math.log( (1 + Math.sin(lat_rad)) / (1 - Math.sin(lat_rad)) ) +128;

次に、世界座標からピクセル座標を求めます。
ここでは、ズームレベルを15とします。

var pixelCoord = new google.maps.Point(0,0);
pixelCoord.x = worldCoord.x * Math.pow(2, 15);
pixelCoord.y = worldCoord.y * Math.pow(2, 15);

あとは、ピクセル座標を256で割れば、商の整数部分がタイル座標となります。

var tileCoord = new google.maps.Point(0,0);
tileCoord.x = Math.floor( pixelCoord.x / 256);
tileCoord.y = Math.floor( pixelCoord.y / 256);

これで、地点を含むタイルを特定できましたが、
そのタイルのなかでの地点の位置も求めておきます。

var innerCoord = new google.maps.Point(0,0);
innerCoord.x = pixelCoord.x % 256;
innerCoord.y = pixelCoord.y % 256;

タイルを1枚貼る

ここでは、600px×600pxのDIV要素をビューポートと見立てます。
ビューポートの中心と指定した地点が合うようにタイルを貼ってみます。

上の座標計算が済んだ状態で、DIV要素(id=”view”)にタイルを貼るコードを考えます。
DOMの操作は分かりづらくなってしまうので、jQueryを使用しました。

$("div#view").empty();
var tileLeft = $("div#view").width() / 2 - innerCoord.x;
var tileTop = $("div#view").height() / 2 - innerCoord.y;
var imgUrl = "http://mt3.google.com/vt?z=15&x=" 
              + tileCoord.x + "&y=" + tileCoord.y; 
var tileImg = $("<img>").attr("src", imgUrl)
                        .width(256).height(256)
                        .css({
                            "position": "relative",
                            "left": tileLeft,
                            "top": tileTop
                  });
$("div#view").append(tileImg);

以上でタイルを一枚だけ選んで、ビューポートに貼りこむことができますが、
前述のとおりグーグルの規約により個別タイルの呼び出しが禁じられていますので、
あくまでも参考に止め、実装はしないでください。

Static Maps を使って擬似タイルをつくる

グーグルマップには、URLパラメータに基づいて地図画像を返してくれるStatic Mapsというサービスがあります。このサービスを使って、擬似的にタイル画像をつくれば、問題なく表示できます。ここでは、画像を呼び出すためのURLを生成する関数を考えます。

Static Mapsは、中心の緯度経度、ズームレベル、サイズ(ここでは256px)をURLにパラメータ指定すると、その画像を返してくれます。そこで、先程のimgUrlを以下のように置き換えるための関数を作成します。

var zoom = 15;
var imgUrl = makeTileUrl(zoom, tileCoord.x, tileCoord.y);

すなわち、タイル座標から、タイル中心の緯度経度を計算する必要があります。
先程とは逆に計算すれば求まります。
緯度経度と世界座標との変換は第3回目を参考にしてください。

        function makeTileUrl(zoom, tileX, tileY) {
            var urlStr = 'https://maps.google.com/maps/api/staticmap?'

            var pixelCoord_c = new google.maps.Point(0, 0);
            pixelCoord_c.x = tileX * 256 + 256 / 2;
            pixelCoord_c.y = tileY * 256 + 256 / 2;

            var worldCoord_c = new google.maps.Point(0, 0);
            worldCoord_c.x = pixelCoord_c.x / Math.pow(2, zoom);
            worldCoord_c.y = pixelCoord_c.y / Math.pow(2, zoom);

            var R = 128 / Math.PI;
            var lat_rad_c = Math.atan( Math.sinh( (128 - worldCoord_c.y) / R ));
            var lng_rad_c = worldCoord_c.x / R - Math.PI;

            var lat_c = lat_rad_c * 180 / Math.PI;
            var lng_c = lng_rad_c * 180 / Math.PI;

            urlStr += 'center=' + lat_c.toFixed(6) + ',' + lng_c.toFixed(6);
            urlStr += '&zoom=' + parseInt(zoom, 10);
            urlStr += '&size=256x256&sensor=false'
            urlStr += '&key=自分のGoogle Maps APIキー'

            return urlStr;
        }

        function sinh(arg) {
            return (Math.exp(arg) - Math.exp(-arg)) / 2;
        }

なお、Static Mapsには、1人の閲覧者が1日にリクエストできる画像(一意の)は1000件までの制限があります。このサイトのような目的ではあまり問題はありませんが、他のプログラムなどで試す場合にはご留意ください。(Google Maps APIの有償化に伴い変更になりました。詳しくはGoogleのサイトをご覧願います)

実際にタイルを1枚選んでみましょう

今回探った仕組みを組み合わせて、ビューポートの中心の位置を含むタイルを1枚だけ
表示するプログラムを作ってみました。地名や住所で検索してみてください。
ズームレベルは15に固定しています。
600px角のビューポートの真ん中には赤い十字が表示してあります。
検索した場所と赤い十字が一致するのが確認できるはずです。

これで、この連載の前編は完了です。
そして、グーグルマップもどきを構築するための準備ができあがりました。
次回からは、実際にタイルを敷き詰めるしくみを探りたいと思います。