使用 PHP 和 Google 試算表建立使用者貢獻的地圖

Google 地圖 API 團隊 Pamela Fox
2007 年 11 月

目標

網路上有許多以地理位置和興趣為中心的社群,例如熱愛博物館、歐洲大教堂、州立公園等的人們。因此,開發人員 (例如您!) 總是需要建立系統,讓使用者在 Google 地圖上提供附有地理標記的地點,而這正是我們在這裡要做的。 本文結束時,您將擁有一個系統,使用者可以在其中註冊、登入及新增附有地理標記的地點。系統會使用 AJAX 做為前端,PHP 做為伺服器端指令碼,並使用 Google 試算表做為儲存空間。如果您習慣使用 MySQL 資料庫進行儲存,可以輕鬆修改此處的程式碼,改用 MySQL 資料庫後端。

本文將分成下列步驟說明:


設定試算表

我們會使用 Google 試算表儲存這個系統的所有資料。我們需要儲存兩種資料:使用者帳戶資訊和使用者新增的地點,因此我們會為每種資料類型建立一個工作表。我們會使用工作表的清單動態饋給與工作表互動,這類動態饋給會依據工作表的第一列 (包含資料欄標籤) 和後續每一列 (包含資料) 運作。

前往 docs.google.com,然後建立新的試算表。將預設工作表重新命名為「Users」,並建立名為「user」、「password」和「session」的資料欄。 然後新增另一個工作表,重新命名為「Locations」,並建立名為「user」、「status」、「lat」、「lng」和「date」的資料欄。或者,如果您不想手動執行這些操作,請下載這個範本,然後透過「檔案」->「匯入」指令匯入 Google 試算表。

使用者帳戶資訊必須設為私人 (只有試算表擁有者 (即您) 才能查看),而使用者新增的地點則會顯示在公開地圖上。幸好,Google 試算表可讓您選擇要公開的試算表工作表,以及要保留為私人的工作表 (預設)。如要發布「地點」工作表,請按一下「發布」分頁標籤,然後按一下「立即發布」,勾選「自動重新發布」核取方塊,接著在「要發布哪些部分?」下拉式選單中,選取「僅限『地點』工作表」。正確選項如下方螢幕截圖所示:

使用 Zend Gdata 架構

Google 試算表 API 提供 HTTP 介面,可執行 CRUD 作業,例如擷取資料列、插入資料列、更新資料列及刪除資料列。Zend Framework 會在 API (和其他 GData API) 上提供 PHP 包裝函式,因此您不必擔心實作原始 HTTP 作業。Zend Framework 需要 PHP 5。

如果沒有 Zend Framework,請下載並上傳至伺服器。如要下載這個架構,請前往: http://framework.zend.com/download/gdata

您應修改 PHP include_path,加入 Zend 程式庫。視伺服器的管理權限層級而定,您可以透過多種方式執行這項操作。其中一個方法是在使用程式庫的任何 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。

建立全域函式

我們為 Community Map 編寫的所有 PHP 指令碼都會使用常見的包含項目、變數和函式,這些項目會放在一個檔案中。

在檔案開頭,我們會加入必要陳述式,以納入並載入 Zend 程式庫,這些陳述式取自 Spreadsheets-ClientLogin.php 範例。

接著,我們會定義檔案中使用的常數:試算表鍵和兩個工作表 ID。如要查看試算表的相關資訊,請開啟試算表,然後依序點選「發布」分頁和「更多發布選項」。從「檔案格式」下拉式清單中選取「ATOM」,然後按一下「產生網址」。您會看到類似下列內容:

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

試算表金鑰是「/list/」後方的長英數字串,工作表 ID 則是後方長度為 3 個字元的字串。如要找出其他工作表 ID,請從「要使用哪些工作表?」下拉式選單中選取其他工作表。

接著,我們將建立 3 個函式:setupClient、getWkshtListFeed 和 printFeed。在 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 變數取得使用者名稱和密碼值。接著設定試算表用戶端,並使用查詢字串要求使用者工作表的清單動態饋給,將結果限制為使用者名稱欄等於傳遞至指令碼的使用者名稱的資料列。如果清單動態饋給結果中沒有任何資料列,表示傳遞的使用者名稱是唯一的,因此可以安全地繼續操作。在清單動態饋給中插入資料列之前,我們會建立欄值的關聯陣列:使用者名稱、使用 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 包裝函式。使用者點按提交按鈕後,我們會從文字欄位取得使用者名稱和密碼,從這些值建構參數字串,並在指令碼網址和參數上呼叫 GDownloadUrl。由於我們要傳送私密資訊,因此使用 GDownloadUrl 的 HTTP POST 版本 (將參數做為第三個引數傳送,而非附加至網址)。在回呼函式中,我們會檢查回應是否成功,並向使用者輸出適當的訊息。

下方顯示範例註冊頁面 (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 變數取得使用者名稱和密碼值。接著設定試算表用戶端,並使用查詢字串要求使用者工作表的清單動態饋給,將結果限制為使用者名稱欄等於傳遞至指令碼的使用者名稱的資料列。

在傳回的資料列中,我們會檢查傳入的密碼雜湊值是否與儲存在試算表中的雜湊值相符。如果是,我們會使用 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 包裝函式。使用者點選提交按鈕時,我們會從文字欄位取得使用者名稱和密碼,並使用查詢參數建構指令碼網址,然後對指令碼網址呼叫 GDownloadUrl。在回呼函式中,我們會設定含有指令碼傳回工作階段 ID 的 Cookie,如果沒有傳回任何 ID,則會輸出錯誤訊息。setCookie 函式來自 cookies.js,該函式是以 w3c JavaScript 教學課程為基礎:http://www.w3schools.com/js/js_cookies.asp。

以下螢幕截圖和程式碼顯示範例登入頁面 (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 網頁,讓他們提供地點資訊,以及兩個 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();
    }
  }
}
?>

在允許使用者新增位置的第二個 PHP 指令碼中,我們首先會複製 communitymap_checksession.php 中的程式碼,確保使用者仍處於登入狀態且有效。然後,一旦我們從使用者工作表取得有效的使用者名稱,就會從 GET 變數取得地點、緯度和經度值。我們將所有這些值放入關聯陣列,並使用 PHP 的 date() 函式新增「date」值,以便瞭解使用者新增地點的時間。我們將該關聯陣列、試算表鍵常數和位置工作表 ID 常數傳遞至 insertRow 函式。如果試算表新增了新地點的資料列,我們就會輸出「成功」。如果在這個步驟發生錯誤,可能是因為欄標題名稱不符。關聯陣列中的鍵必須與試算表鍵和工作表 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 函式擷取工作階段值。如果工作階段字串為空值或空白,我們會輸出錯誤訊息。如果不是,我們會呼叫 map.checksession.php 上的 GDownloadUrl,並傳送工作階段。如果成功傳回使用者名稱,我們會向使用者顯示歡迎訊息、顯示新增地點表單,並載入地圖。表單包含地址文字欄位、地圖,以及地名、緯度和經度文字欄位。如果使用者還不知道該地點的經緯度,可以先在表單中輸入地址,然後按下「提交」進行地理編碼。這會將呼叫傳送至 Map API 的 GClientGeocoder,如果找到地址,系統就會在地圖上放置標記,並自動填入經緯度文字欄位。

使用者確認後,即可按下「新增地點」按鈕。接著,在 JavaScript 中,我們會取得使用者、地點、緯度和經度的值,並透過 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 Spreadsheets 程式庫,應該可以擴充系統,滿足特定需求。如果過程中發生錯誤,請記得您可以在 PHP 中使用 echo 指令,或在 JavaScript 中使用 Map API 的 GLog.write() 進行偵錯,也可以隨時前往 Maps APISpreadsheets API 開發人員論壇尋求額外協助。