帕梅拉·福克斯 (Pamela Fox),Google Maps API 小组
目标
网络上充满了各种各样以不同地区和兴趣为中心的社区,比如喜欢博物馆的人、喜欢欧式教堂的人、喜欢州立公园的人。因此,像您这样的开发人员通常需要创建一个系统,供用户向地图中添加带地理标签的位置,而这正是本文要讨论的内容。读完本文,您就能根据其中介绍的操作说明创建一个系统,供用户注册、登录以及添加带地理标签的位置。在该系统中,前端使用的是 AJAX,服务器端脚本编写使用的是 PHP,存储使用的是 Google 电子表格。如果您习惯使用 MySQL 数据库进行存储,那么只需简单修改一下代码就可以改用 MySQL 数据库后端。
本文分为以下几个步骤:
设置电子表格
使用 Zend Gdata Framework
创建全局函数
注册新用户
用户登录
让用户添加地图位置
创建地图
总结
设置电子表格
我们使用 Google 电子表格存储此系统的所有数据。我们需要存储两种类型的数据(用户帐户信息和用户添加的位置),因此,将会为每种数据类型创建一个工作表。我们使用工作表的列表供稿与这些工作表进行交互,前提是工作表中的第一行包含列标签,其后的各行包含数据。
请访问 docs.google.com 并新建一张电子表格。将默认工作表重命名为“Users”,然后创建名为“user”、“password”和“session”的列。接着再添加另一张工作表,将其重命名为“Locations”,然后创建名为“user”、“status”、“lat”、“lng”和“date”的列。如果您不想手动完成这些工作,可以下载此模版,再通过“文件”->“导入”命令将其导入 Google 电子表格。
用户帐户信息需要保密(只有作为电子表格拥有者的您才可以看到),而用户添加的位置将会显示在公开显示的地图上。好在 Google 电子表格可让您有选择地确定电子表格中的哪些工作表可以公开,哪些需要保密(默认)。要发布“Locations”工作表,请点击“发布”标签,点击“开始发布”,选中“重新发布”复选框,然后在“哪些部分?”下拉列表中选择“仅限“Locations”工作表”。下面的屏幕截图中显示了正确的选项:
使用 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。
您应修改 PHP include_path 以添加 Zend 库。修改的方法有很多种,请根据您在服务器上拥有的管理权限级别进行选择。一种方法是将下面一行代码添加到任一个使用该库的 PHP 文件中的 require 语句之上:
ini_set("include_path", ".:/usr/lib/php:/usr/local/lib/php:../../../library/");
要测试其效果,请在 demos/Zend/Gdata 文件夹的命令行中输入下面一行代码,以便运行 Google 电子表格演示:
php Spreadsheet-ClientLogin.php --user=YourGMailUsername --pass=YourPassword
如果操作成功,屏幕上应显示出一系列电子表格。如果出现错误,请检查是否正确设置了添加路径以及是否安装了 PHP 5。
创建全局函数
为社区地图编写的所有 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 的 Google 电子表格列表供稿,并附带可选的电子表格查询(链接)。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 变量获取用户名和密码的值。然后设置 Google 电子表格客户端,并使用查询字符串来请求用户工作表的列表供稿,以便对查询结果进行限制,使其只显示特定行(其中,用户名列中的用户名等于传递到脚本中的用户名)。如果列表供稿结果中没有行,就可以确认传入的用户名是唯一的,从而可以继续安全地进行操作。在向列表供稿中插入行之前,我们会先创建列值的关联数组:用户名、使用 PHP 的 sha1 函数得到的加密密码以及会话的填充字符。然后对电子表格客户端调用 insertRow,以便传入关联数组、电子表格密钥和工作表 ID。如果返回的对象是 ListFeedEntry,就会输出一条消息“成功!”。
执行此操作的 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" οnclick="register()" value="Register"> <div id="message"></div> </body> </html>
用户登录
为了让用户能够登录系统,我们需要一个面向用户的 HTML 网页(用于提示用户输入自己的用户名和密码)以及一个 PHP 脚本(用于验证登录信息、创建会话 ID 并将其传回登录网页以设置 Cookie)。用户凭借会话 Cookie 可以在后续网页上保持已登录的状态。
在 PHP 脚本中,我们首先加入全局脚本,然后通过 GET 变量获取用户名和密码的值。然后设置 Google 电子表格客户端,并使用查询字符串来请求用户工作表的列表供稿,以便对查询结果进行限制,使其只显示特定行(其中,用户名列中的用户名等于传递到脚本中的用户名)。
在返回的行中,我们将会检查所传入密码的哈希值是否与电子表格中存储的哈希值相匹配。如果匹配,我们就会使用 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 辅导手册中的 cookies.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" οnclick="login()" value="Login"> <div id="message"></div> </body> </html>
让用户添加地图位置
为了让用户能够向地图添加位置,我们需要一个面向用户的 HTML 网页(供用户提供有关该位置的信息)以及两个 PHP 脚本(一个用于检查用户是否通过我们设置的 Cookie 登录,另一个用于将该位置添加到 locations 工作表)。
在第一个检查用户是否登录的 PHP 脚本中,我们首先加入全局脚本,接着通过 GET 变量获取会话值。然后设置 Google 电子表格客户端,并使用查询字符串来请求用户工作表的列表供稿,以便对查询结果进行限制,使其只显示特定行(其中,用户名列中的用户名等于传递到脚本中的用户名)。然后我们循环访问该供稿中与列标题相对应的自定义条目,如果该会话有对应的用户名,则显示其用户名。
执行此操作的 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 中复制代码,目的是确保用户仍然处于登录状态并且有效。在从 users 工作表中取回有效的用户名之后,我们通过 GET 变量获取 place、lat 和 lng 值。接着将上述所有值输入关联数组中,并且使用 PHP 的 date() 函数添加“date”值,这样便可以得知用户添加位置的时间。再将这个关联数组、电子表格密钥常量以及 locations 工作表 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。如果成功返回了一个用户名,则我们会对该用户显示欢迎消息,展现添加位置表单并载入地图。该表单包括地址文本字段、地图以及位置名称、纬度和经度的文本字段。如果用户还不知道位置的纬度/经度,可以在该表单中输入地址并按“submit”对该地址进行地址解析。该操作将会对 Google Maps API 的 GClientGeocoder 发送调用,如果 GClientGeocoder 找到了该地址,则会在地图上做出标记,并自动填充纬度/经度文本字段。
如果用户觉得满意,就可以按下“add location”按钮。然后,我们将会在 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 οnlοad="load()" οnunlοad="GUnload()"> <div id="message"></div> <div id="content" style="display:none"> <form action="#" οnsubmit="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" οnclick="addLocation()" value="Add a location" /> </form> </div> </body> </html>
创建地图
由于您在第一个步骤中就公开了 locations 工作表,因此,无需在服务器端进行编程即可创建它们的地图。事实上,根本就不需要编程。您可以使用根据电子表格生成地图的向导,该向导会生成地图所需的全部代码。该向导会将工作表条目下载到网页中,所用方法是附加指向相应供稿的 JSON 输出的脚本标签;还会指定在 JSON 下载完之后调用的回调函数。查看此处以获取更多信息。
执行该操作的示例 HTML 代码位于 mainmap.htm。屏幕截图如下所示:
总结
如果一切顺利,您现在就可以在服务器上运行由用户提供内容的专有地图系统了。本文提供了此系统主要部分所需的基本代码,但既然您已经熟悉了 Zend 电子表格库,就应该能够扩展该系统以满足您的特定需求。如果您在此过程中碰到错误,请记住可以在 PHP 中使用
echo
命令或在 Javascript 中使用 Google Maps API 的
GLog.write()
进行调试,并且随时可以在 Google Maps API 或 Google 电子表格 API 开发人员论坛上发帖以获得更多帮助。