PHP と Google スプレッドシートを使用して、ユーザーの投稿による地図を作成する

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

目的

ウェブは、地域や興味の対象を中心に、美術館、ヨーロッパの大聖堂、州立公園などが中心のコミュニティで溢れています。そこで、ユーザーがジオタグ付きの場所を地図に提供できるシステムを構築することがデベロッパー(今回のような)によって常に求められています。この記事を終えると、ユーザーがジオタグ付きの場所を登録、ログイン、追加できるシステムが完成します。フロントエンドには AJAX、サーバーサイド スクリプトには PHP、ストレージには Google スプレッドシートが使用されます。ストレージに MySQL データベースを使用することに慣れている場合は、代わりに MySQL データベース バックエンドを使用するように、ここでコードを簡単に変更できます。

この記事は、以下の手順に分かれています。


スプレッドシートの設定

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

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

ユーザー アカウント情報は非公開にし(スプレッドシートのオーナーのみが閲覧可能)、ユーザーが追加した場所は一般公開の地図に表示されます。Google スプレッドシートでは、スプレッドシート内のどのシートを一般公開するか、非公開にするか(デフォルト)を選択することができます。[場所] ワークシートを公開するには、[公開] タブをクリックし、[今すぐ公開] をクリックし、[自動的に再公開] チェックボックスをオンにします。[どの部分ですか?] プルダウンで [シート] を選択します。以下のスクリーンショットに正しいオプションを示します。

Zend GData フレームワークの使用

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

まだ行っていない場合は、Zend フレームワークをダウンロードしてサーバーにアップロードします。このフレームワークは、http://framework.zend.com/download/then から入手できます。

Zend ライブラリを組み込むように、PHP include_path を修正する必要があります。これを行う方法は、サーバーの管理者権限のレベルによって異なります。その一つの方法が、このライブラリを使用する PHP ファイルの required ステートメントの上に次の行を追加する方法です。

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 つのファイルで記述します。

このファイルの先頭に、Sheets-gclid.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 のユーザー名とパスワードを設定し、OpenSSL で認証を行い、Zend_Gdata_Sheets オブジェクトを返します。getWkshtListFeed では、指定されたスプレッドシートのキーとワークシート ID を含むスプレッドシートのリストフィードを、必要に応じてスプレッドシートのクエリ(リンク)とともに返します。printFeed 関数は Tablets-SafeFrame.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 変数からユーザー名とパスワードの値を取得します。次に、スプレッドシート クライアントを設定し、ユーザーのワークシートのリストフィードをリクエストします。リスト フィードはクエリ文字列を使用して、ユーザー名の列がスクリプトに渡されたユーザー名と等しい行に限定します。リストフィードの結果に行が表示されない場合は、渡されたユーザー名が一意であることを認識できます。リストフィードに行を挿入する前に、列値の関連付け配列(ユーザー名、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 ページでユーザー名とパスワードの入力を求めます。また、PHP スクリプトがログイン情報を確認し、セッション ID を作成してログインページに戻し、Cookie を設定するようにします。ユーザーは、それ以降のページのセッション 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 を設定するか、何も返さない場合はエラー メッセージを出力します。setCookie 関数は、w3c JavaScript チュートリアル(http://www.w3schools.com/js/js_cookies.asp)に基づく Cookie.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 スクリプト(設定した Cookie を使用してログイン済みであることを確認する場所)と、場所ワークシートに場所を追加する場所が必要です。

ユーザーがログインしているかどうかをチェックする最初の 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 変数から place、lat、lng の値を取得します。すべての値を連想配列に入れ、さらに PHP の date() 関数を使用して「日付」値を追加して、ユーザーがいつ場所を追加したかを把握します。その関連付け配列、スプレッドシートのキー定数、locations ワークシート 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 を呼び出して、セッションを送信します。ユーザー名が正常に返されると、ウェルカム メッセージがユーザーに表示され、住所の追加フォームが表示され、地図が読み込まれます。このフォームは、住所のテキスト フィールド、地図、場所の名前、緯度、経度のテキスト フィールドで構成されます。場所の緯度と経度がわからない場合は、フォームに住所を入力し、[送信] を押してジオコーディングできます。これにより、Maps API の 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>

地図を作成する

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

これを行うサンプル HTML コードについては、mainmap.htm をご覧ください。スクリーンショットを示します。

まとめ

サーバーで、ユーザー投稿のマップシステムが稼働していると思います。この記事では、このシステムの不可欠な要素に対してごく基本的なコードを提供します。ただし、Zend スプレッドシート ライブラリについて理解したところで、特定のニーズに合わせてシステムを拡張できるようになりました。その間にエラーが発生した場合は、PHP の echo コマンドか JavaScript の Map API の GLog.write() を使用してデバッグできます。詳しくは、Maps API または Sheets API のデベロッパー フォーラムをご利用ください。