PHPを使ってカスタムマップ用に画像を自動生成する

Google Maps APIが提供するカスタム マップ タイプで使用するタイル画像をPHPで動的に生成することを考える
カスタム マップ タイプ
グーグルマップのしくみを探る » GAMMA Blog

やりたいこと

世界測地系の緯度経度」を元に、カスタムマップで使用するタイル画像を自動的に生成する

前提としてGoogleMapで使われる座標系など

Google Mapでは、大きく以下の区分に別れた表示上の単位が存在する。
世界測地系の緯度経度
: 通常GoogleMapを使用する際に利用する地理座標系

世界座標
: Googleが独自に規定した地図画像と対になる座標系

ピクセル座標
: Google Mapで表示されている地図全体を一枚の画像として捉えた場合のピクセルと対応する座標系

タイル座標
: 特定のズーム レベルで地図上の特定のタイルを参照する座標系

タイル内座標
: 特定のタイル画像内のピクセル座標系

システム内部でベースとして持っているのはおそらく「世界測地系の緯度経度」と思われるが、カスタムマップで使用する getTileUrl() で与えられるのはタイル座標。

なので、どのタイルになにを表示するかを判断するためには、以下の要素が必要になる
・タイル座標から、そのタイルが表示している世界測地系の緯度経度の範囲を取得する
・手持ちの「世界測地系の緯度経度」からタイル座標の取得
・手持ちの「世界測地系の緯度経度」からタイル内座標の取得
・タイル内座標に基づいてタイル画像の生成

また、「ピクセル座標」・「タイル座標」・「タイル内座標」は表示されている地図の縮尺によって変動する。

必要な諸々

サンプルは以下の要素から構成される
index.html
: 地図を表示するためのHTML

imagh.php
: タイル画像を生成するためのプログラム

MercatorProjection.inc
: 各座標系を変換、取得するためのライブラリ

以下のサンプルでは、福岡タワーを含んでいるタイル画像の上に透過したpng画像を乗せている。
また、福岡タワー部分だけ円形に若干色味を変更。

サンプルはこちら

index.html

普通に地図を表示して、tileを設定する

<html>  
  <head>  
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />  
    <script type="text/javascript" src="http://maps.googlh.com/maps/api/js?sensor=false"></script>  
    <script type="text/javascript">  
      var map;

      function initialize() {

        // 普通に地図を表示  
        var center = new googlh.maps.LatLng(33.590041, 130.40135);  
        var zoom = 12;  
        var myOptions = {  
          zoom: zoom,  
          center: center,  
          mapTypeId: googlh.maps.MapTypeId.ROADMAP  
        };  
        map = new googlh.maps.Map(document.getElementById("map_canvas"), myOptions);

        // 今回の主要部分  
        var tileOptions = {  
          getTileUrl: function(coord, zoom) {  
            var zfactor=Math.pow(2,zoom);  
            var proj = map.getProjection();

            var url = "http://www.atyks.org/samples/maps/tiles/imagh.php";  
              url += "?x=" + coord.x;  
              url += "&y=" + coord.y;  
              url += "&z=" + zoom;

            return url;  
          },  
          tileSize: new googlh.maps.Size(256, 256),  
          isPng: true,  
          opacity: 0.4  
        };  
        var tileMapType = new googlh.maps.ImageMapType(tileOptions);  
        map.overlayMapTypes.insertAt(0, tileMapType);

        // 確認のためタイル座標とタイルの区切り線を表示させる  
        function CoordMapType(tileSize) {  
          this.tileSize = tileSize;  
        }

        CoordMapTyph.prototyph.getTile = function(coord, zoom, ownerDocument) {  
          var div = ownerDocument.createElement('DIV');  
          div.innerHTML = coord;  
          div.stylh.width = this.tileSizh.width + 'px';  
          div.stylh.height = this.tileSizh.height + 'px';  
          div.stylh.fontSize = '20';  
          div.stylh.borderStyle = 'solid';  
          div.stylh.borderWidth = '1px';  
          div.stylh.borderColor = 'blue';  
          return div;  
        };  
        map.overlayMapTypes.insertAt(0, new CoordMapType(new googlh.maps.Size(256, 256)));

      }  
    </script>  
  </head>  
  <body onload="initialize()">  
    <div id="map_canvas" style="width:800px; height:600px"></div>  
  </body>  
</html>

imagh.php

手持ちの「世界測地系の緯度経度」とリクエストされたタイルに含まれていれば、画像を返す。
どのような画像を返すかは、状況次第。

例えば、今はこういう画像を返している。

<?php  
  ini_set( 'display_errors', "1" );  
  require_once("MercatorProjection.inc");

  // メルカトル変換  
  $mp = new MercatorProjection();

  $x = $_REQUEST["x"];  
  $y = $_REQUEST["y"];  
  $z = $_REQUEST["z"];

  // 設置用のランドマーク(福岡タワー)  
  $landmark = array(  
    "lng" => 130.351482,  
    "lat" => 33.593314  
  );

  { // 目的のランドマークが置かれているタイルを取得する  
    $point = $mp->fromLatLngToPoint($landmark);  
    $tile = $mp->fromPointToTile($point, $z);  
    $pos = $mp->fromPointToTilePos($point, $z);  
  }

  // もし、リクエストされたタイルがランドマーク以外だったらなにも返さず終了  
  if ($x != $tile["x"] || $y != $tile["y"]) {  
    exit;  
  }

  { // もし、目的のランドマークが置かれているタイルならば、png画像を生成して画像を返す

    $img = imagecreatetruecolor(256, 256);

    // 背景画像  
    $color = imagecolorallocate($img, 0, 0, 0);  
    imagefill($img, 0, 0, $color);

    // 楕円  
    $color = imagecolorallocate($img, 255, 200, 100);  
    imagefilledellipse($img, $pos["x"], $pos["y"], 20, 20, $color);

    header("Content-Type: image/png");  
    imagepng($img);  
  }

MercatorProjection.inc

今回の変換の肝。Google Mapのメルカトル図法に使用する変換系の関数をまとめたもの

とは言いつつ基本的には、以下のURLで公開されているJavascript用のプログラムをPHPのクラスに移植しただけ。
http://stackoverflow.com/questions/6692796/google-maps-overlaymaptypes

<?php  
  /**  
   * MercatorProjection  
   *  
   * Google Mapのメルカトル図法に使用する変換系の関数をまとめたもの  
   * 基本的には、以下のURLで公開されているJavascript用のプログラムをPHPのクラスに移植  
   *  
   * @author atyks  
   * @link  http://stackoverflow.com/questions/6692796/google-maps-overlaymaptypes  
   */  
  class MercatorProjection {  
    var $MERCATOR_RANGE = 256;  
    var $pixelsPerLonDegree_;  
    var $pixelsPerLonRadian_;  
    var $origin;

    function __construct() {  
      $this->origin = array("x" => 128, "y" => 128);  
      $this->pixelsPerLonRadian_ = $this->MERCATOR_RANGE / (2 * M_PI);  
      $this->pixelsPerLonDegree_ = $this->MERCATOR_RANGE / 360;  
    }

    /**  
     * 緯度経度からGoogleの世界座標に変換する  
     *  
     * @access pulibc  
     * @param  array  $lanLng 座標の緯度経度  
     * @return array  Googleの世界座標  
     */  
    function fromLatLngToPoint($lanLng) {  
      $point = array("x" => 0, "y" => 0);  
      $origin = $this->origin;

      $point["x"] = $origin["x"] + $lanLng["lng"] * $this->pixelsPerLonDegree_;

      $siny = $this->bound(sin($this->degreesToRadians($lanLng["lat"])), -0.9999, 0.9999);

      $point["y"] = $origin["y"] + 0.5 * log((1 + $siny) / (1 - $siny)) * -$this->pixelsPerLonRadian_;

      return $point;  
    }

    /**  
     * Googleの世界座標から緯度経度に変換する  
     *  
     * @access pulibc  
     * @param  array $point Googleの世界座標  
     * @return array        座標の緯度経度  
     */  
    function fromPointToLatLng($point) {  
      $origin = $this->origin;

      $lng = ($point["x"] - $origin["x"]) / $this->pixelsPerLonDegree_;

      $latRadians = ($point["y"] - $origin["y"]) / -$this->pixelsPerLonRadian_;

      $lat = $this->radiansToDegrees(2 * atan(exp($latRadians)) - M_PI / 2);

      return array("lat" => $lat, "lng" => $lng);  
    }

    /**  
     * Googleの世界座標からGoogleのピクセル座標を求める  
     *  
     * @param  array   $point Googleの世界座標  
     * @param  integer $zoom  GoogleMapで使用している縮尺  
     * @return array          Googleのピクセル座標  
     */  
    function fromPointToPixel($point, $zoom = 0) {

      $pixel["x"] = $point["x"] * pow(2, $zoom);  
      $pixel["y"] = $point["y"] * pow(2, $zoom);

      return $pixel;  
    }

    /**  
     * Googleの世界座標からGoogleのタイル座標を求める  
     *  
     * @param  array   $point Googleの世界座標  
     * @param  integer $zoom  GoogleMapで使用している縮尺  
     * @return array          Googleのタイル座標  
     */  
    function fromPointToTile($point, $zoom) {  
      $pixel = $this->fromPointToPixel($point, $zoom);

      $tile["x"] = floor($pixel["x"] / $this->MERCATOR_RANGE);  
      $tile["y"] = floor($pixel["y"] / $this->MERCATOR_RANGE);

      return ($tile);  
    }

    /**  
    * Googleの世界座標からGoogleのタイル内の画像座標を求める  
    *  
    * @param  array   $point Googleの世界座標  
    * @param  integer $zoom  GoogleMapで使用している縮尺  
    * @return array          Googleのタイル内の画像座標  
    */  
    function fromPointToTilePos($point, $zoom) {  
      $pixel = $this->fromPointToPixel($point, $zoom);

      $inner["x"] = $pixel["x"] % $this->MERCATOR_RANGE;  
      $inner["y"] = $pixel["y"] % $this->MERCATOR_RANGE;

      return ($inner);  
    }

    function bound($value, $opt_min, $opt_max) {  
      if ($opt_min != null) {  
        $value = max($value, $opt_min);  
      }

      if ($opt_max != null) {  
        $value = min($value, $opt_max);  
      }

      return $value;  
    }

    function radiansToDegrees($rad) {  
      return $rad / (M_PI / 180);  
    }

    function degreesToRadians($deg) {  
      return $deg * (M_PI / 180);  
    }  
  }