PHP と Google スプレッドシートでユーザー投稿型地図を作成する

Google Maps API チーム、Pamela Fox
2007 年 11 月

目標

ウェブには、地域や興味関心を中心としたコミュニティが数多く存在します。美術館、ヨーロッパの大聖堂、州立公園などを愛する人々が集まるコミュニティなどです。そのため、ユーザーがジオタグ付きの場所を地図に投稿できるシステムを構築するデベロッパー(あなたのような人)のニーズは常に存在します。ここでは、まさにそのようなシステムを構築します。この記事を読み終えると、ユーザーが登録、ログイン、位置情報タグ付きの場所の追加を行えるシステムが完成します。このシステムでは、フロントエンドに AJAX、サーバーサイド スクリプトに PHP、ストレージに Google スプレッドシートを使用します。ストレージに MySQL データベースを使用している場合は、ここでコードを簡単に変更して、MySQL データベース バックエンドを使用できます。

この記事は、次の手順で構成されています。


スプレッドシートを設定する

このシステムのすべてのデータを保存するために、Google スプレッドシートを使用します。保存する必要があるデータは、ユーザー アカウント情報とユーザーが追加した場所の 2 種類であるため、データタイプごとに 1 つのワークシートを作成します。ワークシートのリスト フィードを使用してワークシートを操作します。リスト フィードは、ワークシートの 1 行目に列ラベルが含まれ、それ以降の各行にデータが含まれていることを前提としています。

docs.google.com にアクセスし、新しいスプレッドシートを作成します。デフォルトのワークシートの名前を「Users」に変更し、「user」、「password」、「session」という名前の列を作成します。次に、別のシートを追加して「Locations」という名前に変更し、「user」、「status」、「lat」、「lng」、「date」という名前の列を作成します。また、手作業が面倒な場合は、こちらのテンプレートをダウンロードして、[ファイル] -> [インポート] コマンドで Google スプレッドシートにインポートします。

ユーザー アカウント情報は非公開(スプレッドシートの所有者である自分のみに表示)にする必要がありますが、ユーザーが追加した場所は一般公開されている地図に表示されます。幸いなことに、Google スプレッドシートでは、スプレッドシート内のどのワークシートを公開し、どのワークシートを非公開(デフォルト)にするかを個別に選択できます。[場所] ワークシートを公開するには、[公開] タブをクリックし、[今すぐ公開] をクリックして、[自動的に再公開する] チェックボックスをオンにします。次に、[どの部分を公開しますか?] プルダウンで [シート「場所」のみ] を選択します。正しいオプションは、次のスクリーンショットのとおりです。

Zend GData Framework の使用

Google スプレッドシート API は、行の取得、行の挿入、行の更新、行の削除などの CRUD オペレーション用の HTTP インターフェースを提供します。Zend Framework は、API(およびその他の GData API)の上に PHP ラッパーを提供するため、生の HTTP オペレーションの実装について心配する必要はありません。Zend Framework には PHP 5 が必要です。

まだお持ちでない場合は、Zend Framework をダウンロードしてサーバーにアップロードします。フレームワークは http://framework.zend.com/download/gdata で入手できます。

Zend ライブラリを含めるように PHP の include_path を変更する必要があります。サーバーで付与されている管理権限のレベルに応じて、いくつかの方法があります。1 つの方法は、ライブラリを使用する PHP ファイルの require ステートメントの上に次の行を追加することです。

ini_set("include_path", ".:/usr/lib/php:/usr/local/lib/php:../../../library/");

テストするには、demos/Zend/Gdata フォルダのコマンドラインで次のコマンドを入力して、スプレッドシートのデモを実行します。

php Spreadsheet-ClientLogin.php --user=YourGMailUsername --pass=YourPassword

正常に動作すると、スプレッドシートのリストが表示されます。エラーが発生した場合は、インクルード パスが正しく設定されていることと、PHP 5 がインストールされていることを確認してください。

グローバル関数の作成

コミュニティ マップ用に作成する PHP スクリプトはすべて、共通のインクルード、変数、関数を使用します。これらは 1 つのファイルにまとめます。

ファイルの先頭には、Spreadsheets-ClientLogin.php の例から取得した、Zend ライブラリを含めて読み込むために必要なステートメントがあります。

次に、ファイル全体で使用される定数(スプレッドシート キーと 2 つのワークシート ID)を定義します。スプレッドシートの情報を確認するには、スプレッドシートを開き、[公開] タブをクリックして、[その他の公開オプション] をクリックします。[ファイル形式] プルダウン リストから [ATOM] を選択し、[URL を生成] をクリックします。次のような出力が表示されます。

http://spreadsheets.google.com/feeds/list/o16162288751915453340.4016005092390554215/od6/public/basic

スプレッドシート キーは「/list/」の後の長い英数字の文字列で、ワークシート ID はその後の 3 文字の文字列です。別のワークシートの ID を確認するには、[どのシートですか?] プルダウンから別のシートを選択します。

次に、setupClient、getWkshtListFeed、printFeed の 3 つの関数を作成します。setupClient では、Gmail のユーザー名とパスワードを設定し、ClientLogin で認証して、Zend_Gdata_Spreadsheets オブジェクトを返します。getWkshtListFeed では、指定されたスプレッドシート キーとワークシート ID のスプレッドシート リスト フィードを返します。スプレッドシート クエリ(リンク)は省略可能です。printFeed 関数は Spreadsheets-ClientLogin.php の例から取得したもので、デバッグに役立つ場合があります。フィード オブジェクトを受け取り、画面に出力します。

この処理を行う PHP は次のとおりです(communitymap_globals.php)。

<?php
ini_set("include_path", ".:/usr/lib/php:/usr/local/lib/php:../../../library/");
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata');
Zend_Loader::loadClass('Zend_Gdata_ClientLogin');
Zend_Loader::loadClass('Zend_Gdata_Spreadsheets');
Zend_Loader::loadClass('Zend_Http_Client');

define("SPREADSHEET_KEY", "o16162288751915453340.4016005092390554215");
define("USER_WORKSHEET_ID", "od6");
define("LOC_WORKSHEET_ID", "od7");
 
function setupClient() {
  $email = "your.name@gmail.com";
  $password = "yourPassword";
  $client = Zend_Gdata_ClientLogin::getHttpClient($email, $password,
          Zend_Gdata_Spreadsheets::AUTH_SERVICE_NAME);
  $gdClient = new Zend_Gdata_Spreadsheets($client);
  return $gdClient;
}
 
function getWkshtListFeed($gdClient, $ssKey, $wkshtId, $queryString=null) {
  $query = new Zend_Gdata_Spreadsheets_ListQuery();
  $query->setSpreadsheetKey($ssKey);
  $query->setWorksheetId($wkshtId);
  if ($queryString !== null) {
    $query->setSpreadsheetQuery($queryString);
  }
  $listFeed = $gdClient->getListFeed($query);
  return $listFeed;
}
 
function printFeed($feed)
{
  print "printing feed";
  $i = 0;
  foreach($feed->entries as $entry) {
      if ($entry instanceof Zend_Gdata_Spreadsheets_CellEntry) {
         print $entry->title->text .' '. $entry->content->text . "\n";
      } else if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
         print $i .' '. $entry->title->text .' '. $entry->content->text . "\n";
      } else {
         print $i .' '. $entry->title->text . "\n";
      }
      $i++;
  }
}
 
?>

新しいユーザーの登録

新しいユーザーを登録するには、テキスト フィールドと送信ボタンを備えたユーザー向けの HTML ページと、ユーザーをスプレッドシートに追加する PHP バックエンド スクリプトが必要です。

PHP スクリプトでは、まずグローバル スクリプトを含め、次に GET 変数からユーザー名とパスワードの値を取得します。次に、スプレッドシート クライアントを設定し、クエリ文字列を使用してユーザー ワークシートのリスト フィードをリクエストします。これにより、ユーザー名列がスクリプトに渡されたユーザー名と等しい行のみに結果が制限されます。リストフィードの結果で 1 行も取得できなかった場合は、渡されたユーザー名が一意であることがわかっているため、安全に続行できます。リストフィードに行を挿入する前に、列の値の連想配列を作成します。ユーザー名、PHP の sha1 関数を使用したパスワードの暗号化、セッションのプレースホルダ文字です。次に、スプレッドシート クライアントで insertRow を呼び出し、連想配列、スプレッドシート キー、ワークシート ID を渡します。返されたオブジェクトが ListFeedEntry の場合は、Success! というメッセージを出力します。

この処理を行う PHP は次のとおりです(communitymap_newuser.php)。

<?php
 
require_once 'communitymap_globals.php';
 
$username = $_GET['username'];
$password = $_GET['password'];
 
$gdClient = setupClient();
 
$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('user='.$username));
$totalResults = $listFeed->totalResults;
if ( $totalResults != "0") {
  // Username already exists
  exit;
}
 
$rowArray["user"] = $username;
$rowArray["password"] = sha1($password);
$rowArray["session"] = "a";
 
$entry = $gdClient->insertRow($rowArray, SPREADSHEET_KEY, USER_WORKSHEET_ID);
if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
  echo "Success!";
}
?>

登録ページに Maps API を含めることで、GDownloadUrl という XMLHttpRequest ラッパー関数を使用できるようになります。ユーザーが送信ボタンをクリックすると、テキスト フィールドからユーザー名とパスワードを取得し、それらの値からパラメータ文字列を構築して、スクリプト URL とパラメータで GDownloadUrl を呼び出します。機密情報を送信するため、GDownloadUrl の HTTP POST バージョンを使用しています(パラメータを URL に追加するのではなく、3 番目の引数として送信します)。コールバック関数では、成功したレスポンスを確認し、適切なメッセージをユーザーに出力します。

以下に、登録ページのサンプル(communitymap_register.htm)のスクリーンショットとコードを示します。


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <title> Community Map - Register/Login </title>

  <script src="http://maps.google.com/maps?file=api&v=2&key=abcdef"
      type="text/javascript"></script>
  <script type="text/javascript">

  function register() {
    var username = document.getElementById("username").value;
    var password = document.getElementById("password").value;
    var url = "communitymap_newuser.php?";
    var params = "username=" + username + "&password=" + password;
    GDownloadUrl(url, function(data, responseCode) {
      if (data.length > 1) {
        document.getElementById("message").innerHTML = "Successfully registered." + 
          "<a href='communitymap_login.htm'>Proceed to Login</a>.";
      } else {
        document.getElementById("message").innerHTML = "Username already exists. Try again.";
      }
    }, params);
  }

  </script>

  </head>
  <body>
  <h1>Register for Community Map</h1>
  <input type="text" id="username">
  <input type="password" id="password">

  <input type="button" onclick="register()" value="Register">
  <div id="message"></div>
  </body>
</html>

ユーザーのログイン

ユーザーがシステムにログインできるようにするには、ユーザーにユーザー名とパスワードの入力を求めるユーザー向けの HTML ページと、ログイン情報を検証し、セッション ID を作成してログインページに渡し、Cookie を設定する PHP スクリプトが必要です。ユーザーは、後続のページでセッション Cookie を通じてログインしたままになります。

PHP スクリプトでは、まずグローバル スクリプトを含め、次に GET 変数からユーザー名とパスワードの値を取得します。次に、スプレッドシート クライアントを設定し、クエリ文字列を使用してユーザー ワークシートのリスト フィードをリクエストします。これにより、ユーザー名列がスクリプトに渡されたユーザー名と等しい行のみに結果が制限されます。

返された行で、渡されたパスワードのハッシュがスプレッドシートに保存されているハッシュと一致することを確認します。セッション ID が存在しない場合は、md5、uniqid、rand 関数を使用してセッション ID を作成します。次に、スプレッドシートの行をセッションで更新し、行の更新が成功した場合は画面に出力します。

その PHP は次のとおりです(communitymap_loginuser.php)。

<?php
 
require_once 'communitymap_globals.php';
 
$username = $_POST['username'];
$password = $_POST['password'];
 
$gdClient = setupClient();
 
$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('user='.$username));
 
$password_hash = sha1($password);
$row = $listFeed->entries[0];
$rowData = $row->getCustom();
foreach($rowData as $customEntry) {
  if ($customEntry->getColumnName()=="password" && $customEntry->getText()==$password_hash) {
    $updatedRowArray["user"] = $username;
    $updatedRowArray["password"] = sha1($password);
    $updatedRowArray["session"] = md5(uniqid(rand(), true));
    $updatedRow = $gdClient->updateRow($row, $updatedRowArray); 
    if ($updatedRow instanceof Zend_Gdata_Spreadsheets_ListEntry) {
      echo $updatedRowArray["session"];
    }
  }
}
?>

ログイン ページで、Maps API を再度含めることで、GDownloadUrl という XMLHttpRequest ラッパー関数を使用できます。ユーザーが送信ボタンをクリックすると、テキスト フィールドからユーザー名とパスワードを取得し、クエリ パラメータを使用してスクリプト URL を作成し、スクリプト URL で GDownloadUrl を呼び出します。コールバック関数では、スクリプトから返されたセッション ID を含む Cookie を設定するか、ID が返されなかった場合はエラー メッセージを出力します。setCookie 関数は、w3c JavaScript チュートリアル(http://www.w3schools.com/js/js_cookies.asp)に基づく cookies.js から取得されます。

以下に、ログインページのサンプル(communitymap_login.htm)のスクリーンショットとコードを示します。


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>

  <title>Community Map - Login</title>
      <script src="http://maps.google.com/maps?file=api&v=2&key=abcdef"
      type="text/javascript"></script>
  <script src="cookies.js" type="text/javascript"></script>
  <script type="text/javascript">

  function login() {
    var username = document.getElementById("username").value;
    var password = document.getElementById("password").value;
    var url = "communitymap_loginuser.php?username=" + username + "&password=" + password;
    GDownloadUrl(url, function(data, responseCode) {
      if (data.length > 1) {
        setCookie("session", data, 5);
      } else {
        document.getElementById("nessage").innerHTML = "Error. Try again.";
      }
    });
  }

  </script>
  </head>
  <body>

  <h1>Login for Community Map</h1>

  <input type="text" id="username">
  <input type="password" id="password">
  <input type="button" onclick="login()" value="Login">
  <div id="message"></div>
  </body>
</html>

ユーザーが地図に場所を追加できるようにする

ユーザーが地図に場所を追加できるようにするには、ユーザー向けの HTML ページを用意して、ユーザーが場所に関する情報を提供できるようにします。また、2 つの PHP スクリプトを用意します。1 つは、設定した Cookie を通じてユーザーがログインしていることを確認するスクリプト、もう 1 つは、場所を場所のワークシートに追加するスクリプトです。

ユーザーがログインしているかどうかを確認する最初の PHP スクリプトでは、まずグローバル スクリプトを含め、次に GET 変数からセッション値を取得します。次に、スプレッドシート クライアントを設定し、クエリ文字列を使用してユーザー ワークシートのリスト フィードをリクエストします。これにより、結果はセッション列がスクリプトに渡されたセッション値と等しい行のみに制限されます。次に、そのフィードのカスタム エントリ(列ヘッダーに対応するもの)を反復処理し、そのセッションに対応するユーザー名が存在する場合は、そのユーザー名を出力します。

その処理を行う PHP は次のとおりです(communitymap_checksession.php)。

<?php

require_once 'communitymap_globals.php';

$session = $_GET['session'];

$gdClient = setupClient();

$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('session='.$session));

if ( count($listFeed->entries) > 0) {
  $row = $listFeed->entries[0];
  $rowData = $row->getCustom();
  foreach($rowData as $customEntry) {
    if ($customEntry->getColumnName()=="user") {
      echo $customEntry->getText();
    }
  }
}
?>

ユーザーが場所を追加できる 2 つ目の PHP スクリプトでは、まず communitymap_checksession.php のコードを複製して、ユーザーがログインしたままで有効であることを確認します。ユーザーシートから有効なユーザー名が返されたら、GET 変数から場所、緯度、経度の値を取得します。これらの値をすべて連想配列に入れ、PHP の date() 関数を使用して「date」値も追加します。これにより、ユーザーが場所を追加した日時を把握できます。この連想配列、スプレッドシートのキー定数、場所のワークシート ID 定数を insertRow 関数に渡します。新しい場所の行がスプレッドシートに追加された場合は、「Success」を出力します。この手順でエラーが発生した場合は、列ヘッダー名の不一致が原因である可能性があります。連想配列のキーは、スプレッドシート キーとワークシート ID で指定されたワークシートの列ヘッダーと一致する必要があります。

その処理を行う PHP は次のとおりです(communitymap_addlocation.php)。

<?php

require_once 'communitymap_globals.php';

$session = $_GET['session'];

$gdClient = setupClient();

$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('session='.$session));

if ( count($listFeed->entries) > 0) {
  $row = $listFeed->entries[0];
  $rowData = $row->getCustom();
  foreach($rowData as $customEntry) {
    if ($customEntry->getColumnName()=="user") {
      $user = $customEntry->getText();
    }
  }

  $place = $_GET['place'];
  $lat = $_GET['lat'];
  $lng = $_GET['lng'];
  $rowArray["user"] = $user;
  $rowArray["place"] = $place;
  $rowArray["lat"] = $lat;
  $rowArray["lng"] = $lng;
  $rowArray["date"] = date("F j, Y, g:i a");
  $entry = $gdClient->insertRow($rowArray, SPREADSHEET_KEY, LOC_WORKSHEET_ID);
  if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
    echo "Success!\n";
  }
}

?>

場所の追加ページで、Maps API を再度含めて、GDownloadUrl を使用して地図を作成できるようにします。ページが読み込まれたら、cookies.js の getCookie 関数を使用してセッション値を取得します。セッション文字列が null または空の場合は、エラー メッセージを出力します。そうでない場合は、map.checksession.php で GDownloadUrl を呼び出し、セッションを送信します。ユーザー名が正常に返された場合は、ユーザーにウェルカム メッセージを表示し、場所を追加するフォームを表示して、地図を読み込みます。フォームは、住所のテキスト フィールド、地図、地名、緯度、経度のテキスト フィールドで構成されています。ユーザーが場所の緯度/経度をまだ知らない場合は、フォームに住所を入力して [送信] を押すことで、ジオコーディングできます。これにより、Map API の GClientGeocoder に呼び出しが送信されます。GClientGeocoder は、住所が見つかった場合は地図上にマーカーを配置し、緯度と経度のテキスト フィールドに自動的に入力します。

ユーザーが満足したら、[場所を追加] ボタンを押します。次に、JavaScript で user、place、lat、lng の値を取得し、GDownloadUrl を使用して communitymap_addlocation.php スクリプトに送信します。

スクリプトが成功を返すと、成功メッセージが画面に出力されます。

以下は、サンプル追加場所ページ(communitymap_addlocation.htm)のスクリーンショットとコードです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
    <title>Community Map - Add a Place!</title>

    <script src="http://maps.google.com/maps?file=api&v=2.x&key=abcdef" type="text/javascript"></script>
    <script src="cookies.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[

    var map = null;
    var geocoder = null; 
    var session = null;

    function load() {
      session = getCookie('session');
      if (session != null && session != "") {
        url = "communitymap_checksession.php?session=" + session;
        GDownloadUrl(url, function(data, responseCode) {
          if (data.length > 0) {
            document.getElementById("message").innerHTML = "Welcome " + data;
            document.getElementById("content").style.display = "block";
            map = new GMap2(document.getElementById("map"));
            map.setCenter(new GLatLng(37.4419, -122.1419), 13);
            geocoder = new GClientGeocoder();
          }
        });
      } else {
        document.getElementById("message").innerHTML = "Error: Not logged in.";
      }
    }

    function addLocation() {
      var place = document.getElementById("place").value;
      var lat = document.getElementById("lat").value;
      var lng = document.getElementById("lng").value;

      var url = "communitymap_addlocation.php?session=" + session + "&place=" + place +
                "&lat=" + lat + "&lng=" + lng;
      GDownloadUrl(url, function(data, responseCode) {
        GLog.write(data);
        if (data.length > 0) {
          document.getElementById("message").innerHTML = "Location added.";
        }
      });
    }

    function showAddress(address) {
      if (geocoder) {
        geocoder.getLatLng(
          address,
          function(point) {
            if (!point) {
              alert(address + " not found");
            } else {
              map.setCenter(point, 13);
              var marker = new GMarker(point, {draggable:true});
              document.getElementById("lat").value = marker.getPoint().lat().toFixed(6);
              document.getElementById("lng").value = marker.getPoint().lng().toFixed(6);

              map.addOverlay(marker);
              GEvent.addListener(marker, "dragend", function() {
                document.getElementById("lat").value = marker.getPoint().lat().toFixed(6);
                document.getElementById("lng").value = marker.getPoint().lng().toFixed(6);
	      });
            }
          }
        );
      }
    }
    //]]>

    </script>

  </head>

  <body onload="load()" onunload="GUnload()">
   <div id="message"></div>
   <div id="content" style="display:none">

   <form action="#" onsubmit="showAddress(this.address.value); return false">
        <p>
        <input type="text" size="60" name="address" value="1600 Amphitheatre Pky, Mountain View, CA" />
        <input type="submit" value="Geocode!" />

    </form>
      </p>

      <div id="map" style="width: 500px; height: 300px"></div>
 
 	Place name: <input type="text" size="20" id="place" value="" />
	<br/>
 	Lat: <input type="text" size="20" id="lat" value="" />
	<br/>

 	Lng: <input type="text" size="20" id="lng" value="" />

        <br/>
	<input type="button" onclick="addLocation()" value="Add a location" />
    </form>
    </div>

  </body>
</html>

地図を作成する

最初のステップで場所のワークシートを公開したため、場所の地図を作成するためにサーバーサイドのプログラミングは必要ありません。実際には、プログラミングは一切必要ありません。この [Spreadsheets] -> [Map] ウィザードを使用すると、地図に必要なすべてのコードが生成されます。ウィザードは、フィードの JSON 出力を指すスクリプトタグを追加し、JSON がダウンロードされたら呼び出されるコールバック関数を指定することで、ワークシートのエントリをページにダウンロードします。詳しくは、こちらをご覧ください。

そのためのサンプル HTML コードは、mainmap.htm で確認できます。以下にスクリーンショットを示します。

まとめ

これで、サーバー上でユーザー投稿型の地図システムが実行されるようになりました。この記事では、このシステムの重要な側面に必要な非常に基本的なコードを提供しましたが、Zend Spreadsheets ライブラリに精通していれば、特定のニーズに合わせてシステムを拡張できるはずです。エラーが発生した場合は、PHP の echo コマンドまたは JavaScript の Map API の GLog.write() をデバッグに使用できます。また、Maps API または Spreadsheets API のデベロッパー フォーラムに投稿して、追加のサポートを受けることもできます。