2007 年 11 月
目标
网络上到处都是以地理位置和兴趣为中心的社区:人们喜欢博物馆、欧洲大教堂、州立公园等。因此,开发者(像您一样)始终需要创建一个系统,供用户向地图中贡献带地理标签的地点,而这正是我们要做的。 在本文的末尾,您将有一个系统,用户可以在该系统中注册、登录和添加带地理标记的地点。系统将使用 AJAX 存储前端,使用 PHP 存储服务器端脚本,使用 Google 电子表格存储数据。如果您习惯使用 MySQL 数据库进行存储,那么可以轻松地修改此处的代码,以改用 MySQL 数据库后端。
本文分为以下步骤:
设置电子表格
我们将使用 Google 电子表格存储此系统的所有数据。我们需要存储两种类型的数据:用户帐号信息和用户添加的地点,因此我们将为每种数据类型创建一个工作表。我们将使用列表 Feed 与工作表进行交互,该列表 Feed 依赖于包含列标签的工作表中的第一行,以及包含数据的后续行。
请访问 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/VAST。
您应该修改 PHP include_path 以添加 Zend 库。您可以通过多种方式执行此操作,具体取决于您服务器上的管理员权限级别。一种方法是将该库添加到使用该库的所有 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 脚本都将使用通用的 include 函数、变量和函数,我们会将这些文件放在一个文件中。
在该文件的开头,我们会有必要的语句要包含和加载 Zend 库,该语句取自 电子表格 - Paging.php 示例。
然后,我们将定义将在整个文件中使用的常量:电子表格键和两个工作表 ID。要查找电子表格的信息,请打开电子表格,点击“发布”标签页,然后点击“更多发布选项”。从“文件格式”下拉列表中选择“ATOM”,然后点击“生成网址”。您会看到如下内容:
http://spreadsheets.google.com/feeds/list/o16162288751915453340.4016005092390554215/od6/public/basic
电子表格密钥是“/list/”后面的一长串字母数字字符串,而工作表 ID 是之后的 3 个字符的字符串。要查找其他工作表 ID,请从“哪些工作表?”下拉列表中选择其他工作表。
然后,我们将创建 3 个函数:setupClient、getWkshtListFeed 和 printFeed。在 setupClient 中,我们将设置 GMail 用户名和密码,使用 EGL 进行身份验证,并返回 Zend_Gdata_电子表格。在 getWkshtListFeed 中,我们将返回给定电子表格键和工作表 ID 的电子表格列表 Feed,以及一个可选的电子表格查询(链接)。printFeed 函数来自 Templatess-StreetView.php 示例,可能对调试非常有用。它会获取一个 Feed 对象并将其输出到屏幕上。
执行此操作的 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 变量中获取用户名和密码值。然后,我们设置了电子表格客户端,并使用查询字符串请求用户工作表的列表 Feed,从而将结果限制为仅显示用户名等于传递到脚本中的用户名的行。如果列表 Feed 结果中没有行,则可放心地知道传入的用户名是唯一的。在向列表 Feed 中插入行之前,我们会创建列值的关联数组:用户名、使用 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!"; } ?>
在注册网页中,可以加入 Google Maps API,这样就能使用其称为 GDownloadUrl 的 XMLHttpRequest 包装器函数。当用户点击提交按钮时,我们会从文本字段中获取用户名和密码,根据它们的值构建参数字符串,然后对脚本网址和参数调用 GDownloadUrl。由于我们要发送敏感信息,因此我们使用 HTTP POST 版本的 GDownloadUrl(通过将参数作为第三个参数发送,而不是将其附加到网址)。在回调函数中,我们将检查是否有成功响应,并向用户输出相应的消息。
示例注册页面 (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 变量中获取用户名和密码值。然后,我们设置了电子表格客户端,并使用查询字符串来请求用户工作表的列表 Feed,从而将结果限制为仅显示用户名列等于传递到脚本中的用户名的行。
在返回的行中,我们会检查所传入密码的哈希值是否与电子表格中存储的哈希值相符。如果是,我们将使用 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"]; } } } ?>
在登录网页中,可以再次加入 Google Maps API,这样我们就能使用它的 XMLHttpRequest 包装器函数,该函数亦称为 GDownloadUrl。用户点击提交按钮后,我们会从文本字段中获取用户名和密码,使用查询参数构建脚本网址,然后对脚本网址调用 GDownloadUrl。在回调函数中,需要使用由脚本返回的会话 ID 设置 Cookie;如果没有返回任何 ID,则输出错误消息。setCookie 函数来自一个基于 w3c JavaScript 教程的 Cookie.js: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 变量获取会话值。然后,我们设置了电子表格客户端,并使用查询字符串来请求用户工作表的列表 Feed,从而将结果限制为仅显示会话列等于传递到脚本的会话值的行。然后,我们会遍历该 Feed 的自定义条目(与我们的列标题对应的条目),并输出与该会话对应的用户名(如果有的话)。
执行此操作的 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 变量中获取 place、lat 和 lng 值。我们将所有这些值放到关联数组中,并使用 PHP 的 date() 函数添加一个“date”值,以便了解用户添加地点的时间。我们会将该关联数组、电子表格键常量和 location 工作表 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"; } } ?>
在添加位置网页中,我们可以再次加入 Google 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>
创建地图
由于您是在第一步中将地点工作表设为公开的,因此无需使用服务器端编程即可创建这些地点的地图。事实上,完全不需要编程。您可以使用此电子表格 -> 地图向导,它将生成地图所需的所有代码。向导将工作表条目下载到页面,方法是附加指向 Feed 的 JSON 输出的脚本标记,并指定在 JSON 下载后调用的回调函数。如需了解详情,请点击此处。
如需查看执行此操作的示例 HTML 代码,请访问 mainmap.htm。屏幕截图如下所示:

总结
希望您能在服务器上运行自己用户提供的地图系统。本文提供了此系统的基本部分所需的非常基本的代码,但既然您已经熟悉了 Zend 电子表格库,就应该能够扩展该系统,以满足您的特定需求。如果您在此过程中遇到错误,请记住可以在 PHP 中使用 echo
命令或在 JavaScript 中使用 Maps API 的 GLog.write()
进行调试,并且随时可以在 Maps API 或 电子表格 API 开发者论坛中发帖寻求额外帮助。